From 8a6913065a58f00e9329ace43d3b5990ceee7b77 Mon Sep 17 00:00:00 2001 From: JasonMetal <935216773@qq.com> Date: Sun, 1 Feb 2026 18:24:17 +0800 Subject: [PATCH] add kafka rabbitmq --- MIGRATION_TEST_REPORT.md | 378 ++++++++++ go.mod | 39 +- go.sum | 87 ++- http-server.go | 13 + internal/app/entity/req/kafkaReq.go | 27 + internal/app/entity/req/rabbitmqReq.go | 85 +++ internal/app/entity/resp/kafkaResp.go | 35 + internal/app/entity/resp/rabbitmqResp.go | 104 +++ internal/app/error/errorFactory.go | 6 +- .../controller/api/kafkaController/kafka.go | 224 ++++++ .../api/rabbitmqController/rabbitmq.go | 535 +++++++++++++ .../app/service/kafkaService/kafkaService.go | 122 +++ .../rabbitmqService/rabbitmqService.go | 184 +++++ manifest/config/bvt/kafka.yml | 18 + manifest/config/bvt/rabbitmq.yml | 38 + manifest/config/local/kafka.yml | 26 + manifest/config/local/rabbitmq.yml | 45 ++ manifest/config/prod/kafka.yml | 18 + manifest/config/prod/rabbitmq.yml | 38 + manifest/config/test/kafka.yml | 18 + manifest/config/test/rabbitmq.yml | 38 + pkg/support-go/bootstrap/app.go | 10 + pkg/support-go/bootstrap/kafka.go | 655 ++++++++++++++++ pkg/support-go/bootstrap/rabbitmq.go | 713 ++++++++++++++++++ routes/api/kafkaRouter/kafka.go | 29 + routes/api/rabbitmqRouter/rabbitmq.go | 75 ++ routes/base.go | 4 + 27 files changed, 3537 insertions(+), 27 deletions(-) create mode 100644 MIGRATION_TEST_REPORT.md create mode 100644 internal/app/entity/req/kafkaReq.go create mode 100644 internal/app/entity/req/rabbitmqReq.go create mode 100644 internal/app/entity/resp/kafkaResp.go create mode 100644 internal/app/entity/resp/rabbitmqResp.go create mode 100644 internal/app/http/controller/api/kafkaController/kafka.go create mode 100644 internal/app/http/controller/api/rabbitmqController/rabbitmq.go create mode 100644 internal/app/service/kafkaService/kafkaService.go create mode 100644 internal/app/service/rabbitmqService/rabbitmqService.go create mode 100644 manifest/config/bvt/kafka.yml create mode 100644 manifest/config/bvt/rabbitmq.yml create mode 100644 manifest/config/local/kafka.yml create mode 100644 manifest/config/local/rabbitmq.yml create mode 100644 manifest/config/prod/kafka.yml create mode 100644 manifest/config/prod/rabbitmq.yml create mode 100644 manifest/config/test/kafka.yml create mode 100644 manifest/config/test/rabbitmq.yml create mode 100644 pkg/support-go/bootstrap/kafka.go create mode 100644 pkg/support-go/bootstrap/rabbitmq.go create mode 100644 routes/api/kafkaRouter/kafka.go create mode 100644 routes/api/rabbitmqRouter/rabbitmq.go diff --git a/MIGRATION_TEST_REPORT.md b/MIGRATION_TEST_REPORT.md new file mode 100644 index 0000000..e1f8478 --- /dev/null +++ b/MIGRATION_TEST_REPORT.md @@ -0,0 +1,378 @@ +# Kafka 和 RabbitMQ 功能迁移测试报告 + +## 迁移日期 +2026-01-20 + +## 迁移概述 +从 `gin-develop-template` 项目的 develop 分支成功迁移了 Kafka 和 RabbitMQ 消息队列支持功能到 `simple-develop-template` 项目。 + +## 迁移内容清单 + +### ✅ 1. Kafka 功能模块 + +#### 服务层 +- ✅ `internal/app/service/kafkaService/kafkaService.go` - Kafka 服务层实现 + - SendMessage - 同步发送消息 + - SendMessageAsync - 异步发送消息 + - SendJSON - 发送JSON格式数据 + - SendBatch - 批量发送消息 + - SendLog - 发送日志消息 + - SendEvent - 发送事件消息 + - SendMetric - 发送指标消息 + - FetchMessages - 查询消息 + - GetTopicPartitions - 获取分区信息 + - GetPartitionOffset - 获取偏移量信息 + +#### 控制器层 +- ✅ `internal/app/http/controller/api/kafkaController/kafka.go` - HTTP 控制器 + - SendMessage - POST /kafka/send + - SendJSON - POST /kafka/send-json + - FetchMessages - GET /kafka/messages + - GetTopicInfo - GET /kafka/topic-info + +#### 路由层 +- ✅ `routes/api/kafkaRouter/kafka.go` - 路由注册 + +#### 实体层 +- ✅ `internal/app/entity/req/kafkaReq.go` - 请求实体 +- ✅ `internal/app/entity/resp/kafkaResp.go` - 响应实体 + +#### Bootstrap 层 +- ✅ `pkg/support-go/bootstrap/kafka.go` - Kafka 核心实现 + - InitKafka - 初始化函数 + - ProducerSync - 同步生产者 + - ProducerAsync - 异步生产者 + - FetchMessages - 消息查询 + - GetTopicPartitions - 分区查询 + - GetPartitionOffset - 偏移量查询 + +### ✅ 2. RabbitMQ 功能模块 + +#### 服务层 +- ✅ `internal/app/service/rabbitmqService/rabbitmqService.go` - RabbitMQ 服务层实现 + - SendMessage - 发送简单消息 + - SendJSON - 发送JSON消息 + - SendToExchange - 发送到交换机 + - PublishFanout - Fanout模式 + - PublishDirect - Direct模式 + - PublishTopic - Topic模式 + - SendTask - 任务消息 + - SendBatch - 批量发送 + - GetQueueInfo - 获取队列信息 + - PeekMessages - 查看消息(不消费) + - ConsumeMessages - 消费消息 + - PurgeQueue - 清空队列 + - DeleteQueue - 删除队列 + +#### 控制器层 +- ✅ `internal/app/http/controller/api/rabbitmqController/rabbitmq.go` - HTTP 控制器 + - SendMessage - POST /rabbitmq/send + - SendJSON - POST /rabbitmq/send-json + - SendToExchange - POST /rabbitmq/send-exchange + - SendFanout - POST /rabbitmq/send-fanout + - SendDirect - POST /rabbitmq/send-direct + - SendTopic - POST /rabbitmq/send-topic + - SendTask - POST /rabbitmq/send-task + - SendBatch - POST /rabbitmq/send-batch + - HealthCheck - GET /rabbitmq/health + - GetQueueInfo - GET /rabbitmq/queue/info + - PeekMessages - GET /rabbitmq/queue/peek + - ConsumeMessages - GET /rabbitmq/queue/consume + - PurgeQueue - POST /rabbitmq/queue/purge + - DeleteQueue - POST /rabbitmq/queue/delete + +#### 路由层 +- ✅ `routes/api/rabbitmqRouter/rabbitmq.go` - 路由注册 + +#### 实体层 +- ✅ `internal/app/entity/req/rabbitmqReq.go` - 请求实体 +- ✅ `internal/app/entity/resp/rabbitmqResp.go` - 响应实体 + +#### Bootstrap 层 +- ✅ `pkg/support-go/bootstrap/rabbitmq.go` - RabbitMQ 核心实现 + - InitRabbitMQ - 初始化函数 + - PublishSimple - 简单消息发布 + - PublishJSON - JSON消息发布 + - PublishToExchange - 交换机消息发布 + - GetQueueInfo - 队列信息查询 + - PeekMessages - 查看消息 + - ConsumeMessages - 消费消息 + - PurgeQueue - 清空队列 + - DeleteQueue - 删除队列 + +### ✅ 3. 配置文件 + +#### Kafka 配置 +- ✅ `manifest/config/local/kafka.yml` +- ✅ `manifest/config/bvt/kafka.yml` +- ✅ `manifest/config/test/kafka.yml` +- ✅ `manifest/config/prod/kafka.yml` + +#### RabbitMQ 配置 +- ✅ `manifest/config/local/rabbitmq.yml` +- ✅ `manifest/config/bvt/rabbitmq.yml` +- ✅ `manifest/config/test/rabbitmq.yml` +- ✅ `manifest/config/prod/rabbitmq.yml` + +### ✅ 4. 初始化集成 + +- ✅ `pkg/support-go/bootstrap/app.go` - 添加了 InitKafka() 和 InitRabbitMQ() 调用 +- ✅ `routes/base.go` - 添加了路由注册 + +### ✅ 5. 依赖管理 + +- ✅ `go.mod` - 添加了以下依赖: + - `github.com/IBM/sarama v1.46.3` - Kafka 客户端 + - `github.com/rabbitmq/amqp091-go v1.10.0` - RabbitMQ 客户端 + +## 编译测试结果 + +### ✅ 编译状态 +所有相关包编译成功: +- ✅ `internal/app/service/kafkaService` - 编译通过 +- ✅ `internal/app/service/rabbitmqService` - 编译通过 +- ✅ `internal/app/http/controller/api/kafkaController` - 编译通过 +- ✅ `internal/app/http/controller/api/rabbitmqController` - 编译通过 +- ✅ `routes/api/kafkaRouter` - 编译通过 +- ✅ `routes/api/rabbitmqRouter` - 编译通过 +- ✅ `pkg/support-go/bootstrap` - 编译通过 + +### 代码质量 +- ✅ 无编译错误 +- ✅ 无语法错误 +- ✅ 导入路径已正确调整 +- ✅ 配置路径已正确调整(从 `config/` 改为 `manifest/config/`) + +## API 接口清单 + +### Kafka API +| 方法 | 路径 | 功能 | +|------|------|------| +| POST | `/kafka/send` | 发送消息 | +| POST | `/kafka/send-json` | 发送JSON消息 | +| GET | `/kafka/messages` | 查询消息 | +| GET | `/kafka/topic-info` | 获取主题信息 | + +### RabbitMQ API +| 方法 | 路径 | 功能 | +|------|------|------| +| POST | `/rabbitmq/send` | 发送消息 | +| POST | `/rabbitmq/send-json` | 发送JSON消息 | +| POST | `/rabbitmq/send-exchange` | 发送到交换机 | +| POST | `/rabbitmq/send-fanout` | Fanout模式 | +| POST | `/rabbitmq/send-direct` | Direct模式 | +| POST | `/rabbitmq/send-topic` | Topic模式 | +| POST | `/rabbitmq/send-task` | 发送任务 | +| POST | `/rabbitmq/send-batch` | 批量发送 | +| GET | `/rabbitmq/health` | 健康检查 | +| GET | `/rabbitmq/queue/info` | 获取队列信息 | +| GET | `/rabbitmq/queue/peek` | 查看消息 | +| GET | `/rabbitmq/queue/consume` | 消费消息 | +| POST | `/rabbitmq/queue/purge` | 清空队列 | +| POST | `/rabbitmq/queue/delete` | 删除队列 | + +## 功能特性 + +### Kafka 功能 +- ✅ 同步/异步消息发送 +- ✅ 批量消息发送 +- ✅ JSON数据自动序列化 +- ✅ 结构化日志发送 +- ✅ 事件消息发送 +- ✅ 指标数据发送 +- ✅ 消息查询功能 +- ✅ 分区信息查询 +- ✅ 偏移量查询 + +### RabbitMQ 功能 +- ✅ 简单队列消息发送 +- ✅ Worker 队列模式 +- ✅ Fanout 广播模式 +- ✅ Direct 路由模式 +- ✅ Topic 主题模式 +- ✅ 批量消息发送 +- ✅ JSON 消息支持 +- ✅ 队列信息查询 +- ✅ 消息查看(Peek) +- ✅ 消息消费(Consume) +- ✅ 队列管理(清空/删除) +- ✅ 健康检查 + +## 路径调整说明 + +### 导入路径调整 +- 源项目: `develop-template/app/...` +- 目标项目: `github.com/JasonMetal/simple-develop-template/internal/app/...` + +### 配置路径调整 +- 源项目: `config/{env}/kafka.yml` +- 目标项目: `manifest/config/{env}/kafka.yml` + +### Bootstrap 路径调整 +- 源项目: `github.com/JasonMetal/submodule-support-go.git/...` +- 目标项目: `github.com/JasonMetal/simple-develop-template/pkg/support-go/...` + +## 已知问题 + +1. ⚠️ 当前项目中没有测试文件,需要后续添加单元测试 +2. ⚠️ 需要确保 Kafka 和 RabbitMQ 服务已启动才能进行功能测试 +3. ⚠️ 配置文件中的连接信息需要根据实际环境调整 + +## 后续建议 + +1. **添加单元测试** + - 为 kafkaService 添加测试 + - 为 rabbitmqService 添加测试 + - 为 bootstrap 层添加测试 + +2. **集成测试** + - 测试 Kafka 消息发送和接收 + - 测试 RabbitMQ 各种消息模式 + - 测试错误处理和重连机制 + +3. **性能测试** + - 测试批量发送性能 + - 测试并发处理能力 + - 测试连接池性能 + +4. **文档完善** + - 添加使用示例 + - 添加最佳实践文档 + - 添加故障排查指南 + +## 迁移文件清单 + +### Kafka 相关文件 (6个) +1. `internal/app/service/kafkaService/kafkaService.go` - 服务层 +2. `internal/app/http/controller/api/kafkaController/kafka.go` - 控制器 +3. `routes/api/kafkaRouter/kafka.go` - 路由 +4. `internal/app/entity/req/kafkaReq.go` - 请求实体 +5. `internal/app/entity/resp/kafkaResp.go` - 响应实体 +6. `pkg/support-go/bootstrap/kafka.go` - Bootstrap实现 + +### RabbitMQ 相关文件 (6个) +1. `internal/app/service/rabbitmqService/rabbitmqService.go` - 服务层 +2. `internal/app/http/controller/api/rabbitmqController/rabbitmq.go` - 控制器 +3. `routes/api/rabbitmqRouter/rabbitmq.go` - 路由 +4. `internal/app/entity/req/rabbitmqReq.go` - 请求实体 +5. `internal/app/entity/resp/rabbitmqResp.go` - 响应实体 +6. `pkg/support-go/bootstrap/rabbitmq.go` - Bootstrap实现 + +### 配置文件 (8个) +1. `manifest/config/local/kafka.yml` +2. `manifest/config/bvt/kafka.yml` +3. `manifest/config/test/kafka.yml` +4. `manifest/config/prod/kafka.yml` +5. `manifest/config/local/rabbitmq.yml` +6. `manifest/config/bvt/rabbitmq.yml` +7. `manifest/config/test/rabbitmq.yml` +8. `manifest/config/prod/rabbitmq.yml` + +### 修改的现有文件 (3个) +1. `pkg/support-go/bootstrap/app.go` - 添加初始化调用 +2. `routes/base.go` - 添加路由注册 +3. `go.mod` - 添加依赖 + +**总计**: 23个文件 + +## 代码质量检查 + +### Go Vet 检查 +- ⚠️ 发现2个警告(非致命): + - `pkg/support-go/bootstrap/app.go:159` - signal.Notify 使用未缓冲通道(已存在的代码) + - `pkg/support-go/bootstrap/grpc.go:161` - signal.Notify 使用未缓冲通道(已存在的代码) +- ✅ 新迁移的代码无警告 + +### 编译检查 +- ✅ 所有新文件编译通过 +- ✅ 无语法错误 +- ✅ 无类型错误 +- ✅ 导入路径正确 + +## 迁移完成度 + +- ✅ 代码迁移: 100% (12个新文件) +- ✅ 配置迁移: 100% (8个配置文件) +- ✅ 路由集成: 100% (已注册) +- ✅ 编译测试: 100% (所有包编译通过) +- ✅ 代码质量: 100% (无新代码警告) +- ⚠️ 单元测试: 0% (需要添加) +- ⚠️ 集成测试: 0% (需要添加) + +## 功能验证清单 + +### Kafka 功能验证 +- [ ] 初始化测试 - 需要 Kafka 服务运行 +- [ ] 同步发送测试 +- [ ] 异步发送测试 +- [ ] JSON 发送测试 +- [ ] 批量发送测试 +- [ ] 消息查询测试 +- [ ] 分区信息查询测试 + +### RabbitMQ 功能验证 +- [ ] 初始化测试 - 需要 RabbitMQ 服务运行 +- [ ] 简单队列发送测试 +- [ ] JSON 发送测试 +- [ ] Fanout 模式测试 +- [ ] Direct 模式测试 +- [ ] Topic 模式测试 +- [ ] 队列信息查询测试 +- [ ] Peek 消息测试 +- [ ] Consume 消息测试 +- [ ] 队列管理测试 + +## 测试建议 + +### 1. 环境准备 +```bash +# 启动 Kafka (Docker) +docker run -d --name kafka -p 9092:9092 apache/kafka:latest + +# 启动 RabbitMQ (Docker) +docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.12-management +``` + +### 2. 功能测试步骤 +1. 启动应用服务 +2. 测试 Kafka 初始化(检查日志) +3. 测试 RabbitMQ 初始化(检查日志) +4. 使用 Postman 或 curl 测试各个 API 接口 +5. 验证消息发送和接收 + +### 3. 测试命令示例 +```bash +# 测试 Kafka 发送消息 +curl -X POST http://localhost:8080/kafka/send \ + -H "Content-Type: application/json" \ + -d '{"topic":"test-topic","message":"Hello Kafka"}' + +# 测试 RabbitMQ 发送消息 +curl -X POST http://localhost:8080/rabbitmq/send \ + -H "Content-Type: application/json" \ + -d '{"queue_name":"test-queue","message":"Hello RabbitMQ"}' +``` + +## 总结 + +Kafka 和 RabbitMQ 消息队列功能已成功迁移到当前项目。所有代码文件已创建,配置已添加,路由已注册,编译测试通过。代码已准备好进行功能测试和集成测试。 + +### 迁移统计 +- **新增文件**: 20个 +- **修改文件**: 3个 +- **代码行数**: 约 2000+ 行 +- **API 接口**: 18个 +- **配置文件**: 8个 + +### 下一步 +1. 启动 Kafka 和 RabbitMQ 服务 +2. 运行应用并测试各个 API 接口 +3. 根据实际使用情况调整配置 +4. 添加单元测试和集成测试 + +--- + +**报告生成时间**: 2026-01-20 +**迁移状态**: ✅ 完成 +**编译状态**: ✅ 通过 +**代码质量**: ✅ 良好 diff --git a/go.mod b/go.mod index e550887..3d37e08 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,21 @@ module github.com/JasonMetal/simple-develop-template -go 1.22.1 +go 1.24.0 + +toolchain go1.24.12 require ( + github.com/IBM/sarama v1.46.3 github.com/Nerzal/swag v1.16.0 github.com/gin-gonic/gin v1.9.1 + github.com/go-playground/validator/v10 v10.14.0 github.com/go-redis/redis/v8 v8.11.5 github.com/golang/protobuf v1.5.4 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/hashicorp/consul/api v1.28.2 - github.com/json-iterator/go v1.1.12 github.com/leeqvip/gophp v1.1.1 github.com/olebedev/config v0.0.0-20220822221314-86fa169f9f99 + github.com/rabbitmq/amqp091-go v1.10.0 github.com/satori/go.uuid v1.2.0 github.com/spf13/cobra v1.8.0 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.911 @@ -20,7 +24,7 @@ require ( github.com/tencentyun/qcloud-cos-sts-sdk v0.0.0-20240307081512-0195d273fdb5 go.uber.org/zap v1.27.0 google.golang.org/grpc v1.63.2 - google.golang.org/protobuf v1.33.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 gorm.io/driver/mysql v1.5.6 gorm.io/gorm v1.25.10 @@ -33,7 +37,11 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/clbanning/mxj v1.8.4 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/eapache/go-resiliency v1.7.0 // indirect + github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect + github.com/eapache/queue v1.1.0 // indirect github.com/fatih/color v1.14.1 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -43,9 +51,9 @@ require ( github.com/go-openapi/swag v0.22.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/go-querystring v1.0.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -53,12 +61,20 @@ require ( github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/serf v0.10.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.1 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -71,19 +87,22 @@ require ( github.com/mozillazg/go-httpheader v0.2.1 // indirect github.com/opentracing/opentracing-go v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.19.0 // indirect + golang.org/x/crypto v0.43.0 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect - golang.org/x/net v0.21.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.37.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 877aea0..d72cf3b 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/IBM/sarama v1.46.3 h1:njRsX6jNlnR+ClJ8XmkO+CM4unbrNr/2vB5KK6UA+IE= +github.com/IBM/sarama v1.46.3/go.mod h1:GTUYiF9DMOZVe3FwyGT+dtSPceGFIgA+sPc5u6CBwko= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Nerzal/swag v1.16.0 h1:JeBrfXvioJwo6HRu2KoFInk0/0UQ3jfNKtinAkgCVVs= @@ -46,6 +48,12 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= +github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -55,6 +63,8 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= @@ -108,6 +118,8 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= @@ -122,6 +134,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/hashicorp/consul/api v1.28.2 h1:mXfkRHrpHN4YY3RqL09nXU1eHKLNiuAN4kHvDQ16k/8= @@ -155,6 +169,7 @@ github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjG github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= @@ -170,6 +185,18 @@ github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -183,14 +210,17 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -256,6 +286,8 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0Mw github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -277,6 +309,12 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= @@ -304,8 +342,9 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.911 h1:dRkofaSMvvyrfi5bUA2RiV05HYMfYd1nFk0Y1L+VMGs= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.911/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= @@ -323,6 +362,7 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -341,8 +381,10 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= @@ -352,8 +394,9 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -363,12 +406,16 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -377,6 +424,9 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -395,23 +445,31 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -422,8 +480,9 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E= -golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/http-server.go b/http-server.go index d65c602..0785e1f 100644 --- a/http-server.go +++ b/http-server.go @@ -5,6 +5,7 @@ import ( "github.com/JasonMetal/simple-develop-template/pkg/support-go/bootstrap" router "github.com/JasonMetal/simple-develop-template/routes" "github.com/gin-gonic/gin" + "log" ) // ProjectName @@ -16,6 +17,18 @@ func main() { bootstrap.SetProjectName(constant.ProjectName) // 初始化Web bootstrap.Init() + + // ✅ 设置错误处理器 + bootstrap.SetAsyncErrorHandler(func(topic, message string, err error) { + log.Printf("❌ Kafka发送失败 - Topic: %s, Error: %v", topic, err) + + // 你的处理逻辑: + // 1. 保存到重试队列 + // 2. 发送告警 + // 3. 记录日志 + }) + // 启动服务... + middleFun := []gin.HandlerFunc{ // middleware.CheckUserAuth(), } diff --git a/internal/app/entity/req/kafkaReq.go b/internal/app/entity/req/kafkaReq.go new file mode 100644 index 0000000..7e8c048 --- /dev/null +++ b/internal/app/entity/req/kafkaReq.go @@ -0,0 +1,27 @@ +package req + +// SendMessageReq 发送消息请求 +type SendMessageReq struct { + Topic string `json:"topic" binding:"required" example:"test-topic"` + Message string `json:"message" binding:"required" example:"Hello Kafka"` +} + +// SendJSONReq 发送JSON消息请求 +type SendJSONReq struct { + Topic string `json:"topic" binding:"required" example:"test-json"` + Data map[string]interface{} `json:"data" binding:"required"` +} + +// FetchMessagesReq 查询消息请求 +type FetchMessagesReq struct { + Topic string `form:"topic" binding:"required" example:"test-topic"` + Partition int32 `form:"partition" example:"0"` + Offset int64 `form:"offset" example:"0"` + Limit int `form:"limit" example:"10"` + Page int `form:"page" example:"1"` +} + +// GetTopicInfoReq 获取主题信息请求 +type GetTopicInfoReq struct { + Topic string `form:"topic" binding:"required" example:"test-topic"` +} diff --git a/internal/app/entity/req/rabbitmqReq.go b/internal/app/entity/req/rabbitmqReq.go new file mode 100644 index 0000000..efca3ff --- /dev/null +++ b/internal/app/entity/req/rabbitmqReq.go @@ -0,0 +1,85 @@ +package req + +// RabbitMQSendMessageReq 发送消息请求 +type RabbitMQSendMessageReq struct { + QueueName string `json:"queue_name" binding:"required" example:"test-queue"` + Message string `json:"message" binding:"required" example:"Hello RabbitMQ"` +} + +// RabbitMQSendJSONReq 发送JSON消息请求 +type RabbitMQSendJSONReq struct { + QueueName string `json:"queue_name" binding:"required" example:"test-json-queue"` + Data map[string]interface{} `json:"data" binding:"required"` +} + +// SendToExchangeReq 发送消息到交换机请求 +type SendToExchangeReq struct { + ExchangeName string `json:"exchange_name" binding:"required" example:"test-exchange"` + ExchangeType string `json:"exchange_type" binding:"required" example:"direct"` + RoutingKey string `json:"routing_key" example:"test.routing.key"` + Message string `json:"message" binding:"required" example:"Hello Exchange"` +} + +// SendFanoutReq 发送广播消息请求 +type SendFanoutReq struct { + ExchangeName string `json:"exchange_name" binding:"required" example:"fanout-exchange"` + Message string `json:"message" binding:"required" example:"Broadcast Message"` +} + +// SendDirectReq 发送直接消息请求 +type SendDirectReq struct { + ExchangeName string `json:"exchange_name" binding:"required" example:"direct-exchange"` + RoutingKey string `json:"routing_key" binding:"required" example:"error"` + Message string `json:"message" binding:"required" example:"Error Log"` +} + +// SendTopicReq 发送主题消息请求 +type SendTopicReq struct { + ExchangeName string `json:"exchange_name" binding:"required" example:"topic-exchange"` + RoutingKey string `json:"routing_key" binding:"required" example:"user.created"` + Message string `json:"message" binding:"required" example:"User Created Event"` +} + +// SendTaskReq 发送任务消息请求 +type SendTaskReq struct { + QueueName string `json:"queue_name" binding:"required" example:"task-queue"` + TaskName string `json:"task_name" binding:"required" example:"send_email"` + TaskData map[string]interface{} `json:"task_data" binding:"required"` +} + +// SendBatchReq 批量发送消息请求 +type SendBatchReq struct { + QueueName string `json:"queue_name" binding:"required" example:"batch-queue"` + Messages []string `json:"messages" binding:"required,min=1"` +} + +// ============= 查询相关请求 ============= + +// GetQueueInfoReq 获取队列信息请求 +type GetQueueInfoReq struct { + Queue string `form:"queue" json:"queue" binding:"required" example:"test-queue"` // 队列名称 +} + +// PeekMessagesReq 查看消息请求(不消费) +type PeekMessagesReq struct { + Queue string `form:"queue" json:"queue" binding:"required" example:"test-queue"` // 队列名称 + Limit int `form:"limit" json:"limit" example:"10"` // 查看数量,默认10,最多100 +} + +// ConsumeMessagesReq 消费消息请求(消费并删除) +type ConsumeMessagesReq struct { + Queue string `form:"queue" json:"queue" binding:"required" example:"test-queue"` // 队列名称 + Limit int `form:"limit" json:"limit" example:"10"` // 消费数量,默认10,最多100 +} + +// PurgeQueueReq 清空队列请求 +type PurgeQueueReq struct { + Queue string `json:"queue" binding:"required" example:"test-queue"` // 队列名称 +} + +// DeleteQueueReq 删除队列请求 +type DeleteQueueReq struct { + Queue string `json:"queue" binding:"required" example:"test-queue"` // 队列名称 + IfUnused bool `json:"if_unused" example:"false"` // 仅在没有消费者时删除 + IfEmpty bool `json:"if_empty" example:"false"` // 仅在队列为空时删除 +} diff --git a/internal/app/entity/resp/kafkaResp.go b/internal/app/entity/resp/kafkaResp.go new file mode 100644 index 0000000..a43dcbc --- /dev/null +++ b/internal/app/entity/resp/kafkaResp.go @@ -0,0 +1,35 @@ +package resp + +import "github.com/JasonMetal/simple-develop-template/internal/app/entity" + +// KafkaMessageResp Kafka消息响应 +type KafkaMessageResp struct { + Topic string `json:"topic"` + Partition int32 `json:"partition"` + Offset int64 `json:"offset"` + Key string `json:"key"` + Value string `json:"value"` + Timestamp int64 `json:"timestamp"` +} + +// FetchMessagesResp 查询消息响应 +type FetchMessagesResp struct { + Messages []*KafkaMessageResp `json:"messages"` + Pagination entity.Pagination `json:"pagination"` + Topic string `json:"topic"` + Partition int32 `json:"partition"` +} + +// TopicInfoResp 主题信息响应 +type TopicInfoResp struct { + Topic string `json:"topic"` + Partitions []PartitionInfo `json:"partitions"` +} + +// PartitionInfo 分区信息 +type PartitionInfo struct { + Partition int32 `json:"partition"` + OldestOffset int64 `json:"oldest_offset"` + NewestOffset int64 `json:"newest_offset"` + MessageCount int64 `json:"message_count"` +} diff --git a/internal/app/entity/resp/rabbitmqResp.go b/internal/app/entity/resp/rabbitmqResp.go new file mode 100644 index 0000000..05bf17c --- /dev/null +++ b/internal/app/entity/resp/rabbitmqResp.go @@ -0,0 +1,104 @@ +package resp + +// RabbitMQMessageResp RabbitMQ消息响应 +type RabbitMQMessageResp struct { + QueueName string `json:"queue_name"` + Message string `json:"message"` + SentAt string `json:"sent_at"` + MessageCount int `json:"message_count,omitempty"` +} + +// RabbitMQExchangeResp 交换机消息响应 +type RabbitMQExchangeResp struct { + ExchangeName string `json:"exchange_name"` + ExchangeType string `json:"exchange_type"` + RoutingKey string `json:"routing_key"` + Message string `json:"message"` + SentAt string `json:"sent_at"` +} + +// RabbitMQTaskResp 任务消息响应 +type RabbitMQTaskResp struct { + QueueName string `json:"queue_name"` + TaskName string `json:"task_name"` + TaskID string `json:"task_id"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` +} + +// RabbitMQBatchResp 批量发送响应 +type RabbitMQBatchResp struct { + QueueName string `json:"queue_name"` + TotalCount int `json:"total_count"` + SuccessCount int `json:"success_count"` + FailedCount int `json:"failed_count"` + SentAt string `json:"sent_at"` +} + +// RabbitMQHealthResp 健康检查响应 +type RabbitMQHealthResp struct { + Status string `json:"status"` + Connected bool `json:"connected"` + Message string `json:"message,omitempty"` +} + +// ============= 查询相关响应 ============= + +// QueueInfoResp 队列信息响应 +type QueueInfoResp struct { + Name string `json:"name"` // 队列名称 + Messages int `json:"messages"` // 消息数量 + Consumers int `json:"consumers"` // 消费者数量 + Durable bool `json:"durable"` // 是否持久化 + AutoDelete bool `json:"auto_delete"` // 是否自动删除 + Exclusive bool `json:"exclusive"` // 是否排他 +} + +// QueueMessageResp 队列消息响应 +type QueueMessageResp struct { + Body string `json:"body"` // 消息体 + ContentType string `json:"content_type"` // 内容类型 + DeliveryMode uint8 `json:"delivery_mode"` // 投递模式:1=非持久化, 2=持久化 + Priority uint8 `json:"priority"` // 优先级 + CorrelationId string `json:"correlation_id"` // 关联ID + ReplyTo string `json:"reply_to"` // 回复队列 + Expiration string `json:"expiration"` // 过期时间 + MessageId string `json:"message_id"` // 消息ID + Timestamp string `json:"timestamp"` // 时间戳 + Type string `json:"type"` // 消息类型 + UserId string `json:"user_id"` // 用户ID + AppId string `json:"app_id"` // 应用ID + Headers map[string]string `json:"headers"` // 消息头 + DeliveryTag uint64 `json:"delivery_tag"` // 投递标签 + Redelivered bool `json:"redelivered"` // 是否重新投递 + Exchange string `json:"exchange"` // 交换机 + RoutingKey string `json:"routing_key"` // 路由键 +} + +// PeekMessagesResp 查看消息响应 +type PeekMessagesResp struct { + Queue string `json:"queue"` // 队列名称 + Total int `json:"total"` // 返回的消息数量 + Messages []QueueMessageResp `json:"messages"` // 消息列表 +} + +// ConsumeMessagesResp 消费消息响应 +type ConsumeMessagesResp struct { + Queue string `json:"queue"` // 队列名称 + Total int `json:"total"` // 消费的消息数量 + Messages []QueueMessageResp `json:"messages"` // 消息列表 +} + +// PurgeQueueResp 清空队列响应 +type PurgeQueueResp struct { + Queue string `json:"queue"` // 队列名称 + DeletedCount int `json:"deleted_count"` // 删除的消息数量 + Success bool `json:"success"` // 是否成功 +} + +// DeleteQueueResp 删除队列响应 +type DeleteQueueResp struct { + Queue string `json:"queue"` // 队列名称 + DeletedCount int `json:"deleted_count"` // 队列中删除的消息数量 + Success bool `json:"success"` // 是否成功 +} diff --git a/internal/app/error/errorFactory.go b/internal/app/error/errorFactory.go index e4b64c6..4f73ddf 100644 --- a/internal/app/error/errorFactory.go +++ b/internal/app/error/errorFactory.go @@ -2,7 +2,7 @@ package error import ( "github.com/JasonMetal/simple-develop-template/internal/app/error/mysql" - "github.com/JasonMetal/simple-develop-template/internal/app/error/newsPageError" + // "github.com/JasonMetal/simple-develop-template/internal/app/error/newsPageError" // 暂时注释,包不存在 ) // NewMysqlError 实例化mysql错误 @@ -15,12 +15,12 @@ func NewMysqlError() Error { // NewNewsPageError 实例化mysql错误 func NewNewsPageError() Error { return &MyError{ - msgList: newsPageError.ErrorMessageList(), + msgList: mysql.ErrorMessageList(), // 暂时使用mysql的错误列表 } } func NewTypeError() Error { return &MyError{ - msgList: newsPageError.ErrorMessageList(), + msgList: mysql.ErrorMessageList(), // 暂时使用mysql的错误列表 } } diff --git a/internal/app/http/controller/api/kafkaController/kafka.go b/internal/app/http/controller/api/kafkaController/kafka.go new file mode 100644 index 0000000..cd75cbd --- /dev/null +++ b/internal/app/http/controller/api/kafkaController/kafka.go @@ -0,0 +1,224 @@ +package kafkaController + +import ( + "fmt" + "github.com/JasonMetal/simple-develop-template/internal/app/entity" + "github.com/JasonMetal/simple-develop-template/internal/app/entity/req" + "github.com/JasonMetal/simple-develop-template/internal/app/entity/resp" + baseController "github.com/JasonMetal/simple-develop-template/internal/app/http/controller" + "github.com/JasonMetal/simple-develop-template/internal/app/service/kafkaService" + "github.com/gin-gonic/gin" + "math" +) + +type controller struct { + baseController.BaseController +} + +func NewController(ctx *gin.Context) *controller { + return &controller{baseController.NewBaseController(ctx)} +} + +// SendMessage 发送消息 +// @Summary 发送Kafka消息 +// @Description 发送单条消息到指定主题 +// @Tags Kafka +// @Accept json +// @Produce json +// @Param request body req.SendMessageReq true "发送消息请求" +// @Success 200 {object} controller.ResJson +// @Router /kafka/send [post] +func (c *controller) SendMessage() { + var request req.SendMessageReq + if err := c.GCtx.ShouldBindJSON(&request); err != nil { + c.Fail400("参数错误: " + err.Error()) + return + } + + service := kafkaService.NewKafkaService() + if err := service.SendMessage(request.Topic, request.Message); err != nil { + c.Fail400(fmt.Sprintf("发送消息失败: %v", err)) + return + } + + c.SuccessWithMsg(gin.H{ + "topic": request.Topic, + "message": request.Message, + }, "消息发送成功") +} + +// SendJSON 发送JSON消息 +// @Summary 发送JSON格式的Kafka消息 +// @Description 发送JSON格式数据到指定主题 +// @Tags Kafka +// @Accept json +// @Produce json +// @Param request body req.SendJSONReq true "发送JSON消息请求" +// @Success 200 {object} controller.ResJson +// @Router /kafka/send-json [post] +func (c *controller) SendJSON() { + var request req.SendJSONReq + if err := c.GCtx.ShouldBindJSON(&request); err != nil { + c.Fail400("参数错误: " + err.Error()) + return + } + + service := kafkaService.NewKafkaService() + if err := service.SendJSON(request.Topic, request.Data); err != nil { + c.Fail400(fmt.Sprintf("发送JSON消息失败: %v", err)) + return + } + + c.SuccessWithMsg(gin.H{ + "topic": request.Topic, + "data": request.Data, + }, "JSON消息发送成功") +} + +// FetchMessages 查询消息 +// @Summary 查询Kafka消息 +// @Description 查询指定主题的消息,支持分页 +// @Tags Kafka +// @Accept json +// @Produce json +// @Param topic query string true "主题名称" +// @Param partition query int false "分区编号" default(0) +// @Param page query int false "页码" default(1) +// @Param limit query int false "每页数量" default(10) +// @Success 200 {object} controller.ResJson{data=resp.FetchMessagesResp} +// @Router /kafka/messages [get] +func (c *controller) FetchMessages() { + var request req.FetchMessagesReq + if err := c.GCtx.ShouldBindQuery(&request); err != nil { + c.Fail400("参数错误: " + err.Error()) + return + } + + // 设置默认值 + if request.Limit <= 0 { + request.Limit = 10 + } + if request.Limit > 100 { + request.Limit = 100 + } + if request.Page <= 0 { + request.Page = 1 + } + + service := kafkaService.NewKafkaService() + + // 如果没有指定offset,使用page计算 + if request.Offset == 0 && request.Page > 1 { + // 获取分区的最早偏移量 + oldestOffset, _, err := service.GetPartitionOffset(request.Topic, request.Partition) + if err != nil { + c.Fail400(fmt.Sprintf("获取分区偏移量失败: %v", err)) + return + } + request.Offset = oldestOffset + int64((request.Page-1)*request.Limit) + } + + // 获取消息 + messages, err := service.FetchMessages(request.Topic, request.Partition, request.Offset, request.Limit) + if err != nil { + c.Fail400(fmt.Sprintf("查询消息失败: %v", err)) + return + } + + // 获取分区的偏移量信息计算总数 + oldestOffset, newestOffset, err := service.GetPartitionOffset(request.Topic, request.Partition) + if err != nil { + oldestOffset = 0 + newestOffset = 0 + } + + total := newestOffset - oldestOffset + if total < 0 { + total = 0 + } + + lastPage := int(math.Ceil(float64(total) / float64(request.Limit))) + if lastPage < 1 { + lastPage = 1 + } + + // 转换消息格式 + respMessages := make([]*resp.KafkaMessageResp, 0, len(messages)) + for _, msg := range messages { + respMessages = append(respMessages, &resp.KafkaMessageResp{ + Topic: msg.Topic, + Partition: msg.Partition, + Offset: msg.Offset, + Key: msg.Key, + Value: msg.Value, + Timestamp: msg.Timestamp, + }) + } + + response := resp.FetchMessagesResp{ + Messages: respMessages, + Topic: request.Topic, + Partition: request.Partition, + Pagination: entity.Pagination{ + Total: total, + Page: request.Page, + LastPage: lastPage, + }, + } + + c.Success(response) +} + +// GetTopicInfo 获取主题信息 +// @Summary 获取主题信息 +// @Description 获取主题的分区信息和偏移量 +// @Tags Kafka +// @Accept json +// @Produce json +// @Param topic query string true "主题名称" +// @Success 200 {object} controller.ResJson{data=resp.TopicInfoResp} +// @Router /kafka/topic-info [get] +func (c *controller) GetTopicInfo() { + var request req.GetTopicInfoReq + if err := c.GCtx.ShouldBindQuery(&request); err != nil { + c.Fail400("参数错误: " + err.Error()) + return + } + + service := kafkaService.NewKafkaService() + + // 获取分区列表 + partitions, err := service.GetTopicPartitions(request.Topic) + if err != nil { + c.Fail400(fmt.Sprintf("获取分区信息失败: %v", err)) + return + } + + // 获取每个分区的偏移量信息 + partitionInfos := make([]resp.PartitionInfo, 0, len(partitions)) + for _, partition := range partitions { + oldestOffset, newestOffset, err := service.GetPartitionOffset(request.Topic, partition) + if err != nil { + continue + } + + messageCount := newestOffset - oldestOffset + if messageCount < 0 { + messageCount = 0 + } + + partitionInfos = append(partitionInfos, resp.PartitionInfo{ + Partition: partition, + OldestOffset: oldestOffset, + NewestOffset: newestOffset, + MessageCount: messageCount, + }) + } + + response := resp.TopicInfoResp{ + Topic: request.Topic, + Partitions: partitionInfos, + } + + c.Success(response) +} diff --git a/internal/app/http/controller/api/rabbitmqController/rabbitmq.go b/internal/app/http/controller/api/rabbitmqController/rabbitmq.go new file mode 100644 index 0000000..d6ff7e6 --- /dev/null +++ b/internal/app/http/controller/api/rabbitmqController/rabbitmq.go @@ -0,0 +1,535 @@ +package rabbitmqController + +import ( + "fmt" + "github.com/JasonMetal/simple-develop-template/internal/app/entity/req" + "github.com/JasonMetal/simple-develop-template/internal/app/entity/resp" + baseController "github.com/JasonMetal/simple-develop-template/internal/app/http/controller" + "github.com/JasonMetal/simple-develop-template/internal/app/service/rabbitmqService" + "time" + + "github.com/gin-gonic/gin" +) + +type controller struct { + baseController.BaseController +} + +func NewController(ctx *gin.Context) *controller { + return &controller{baseController.NewBaseController(ctx)} +} + +// SendMessage 发送消息 +// @Summary 发送RabbitMQ消息 +// @Description 发送单条消息到指定队列 +// @Tags RabbitMQ +// @Accept json +// @Produce json +// @Param request body req.SendMessageReq true "发送消息请求" +// @Success 200 {object} controller.ResJson +// @Router /rabbitmq/send [post] +func (c *controller) SendMessage() { + var request req.RabbitMQSendMessageReq + if err := c.GCtx.ShouldBindJSON(&request); err != nil { + // logger removed("参数绑定失败: %v", err) + c.Fail400("参数错误: " + err.Error()) + return + } + + service := rabbitmqService.NewRabbitMQService() + if err := service.SendMessage(request.QueueName, request.Message); err != nil { + // logger removed("发送消息失败: %v", err) + c.Fail400(fmt.Sprintf("发送消息失败: %v", err)) + return + } + + c.SuccessWithMsg(resp.RabbitMQMessageResp{ + QueueName: request.QueueName, + Message: request.Message, + SentAt: time.Now().Format(time.RFC3339), + }, "消息发送成功") +} + +// SendJSON 发送JSON消息 +// @Summary 发送JSON格式的RabbitMQ消息 +// @Description 发送JSON格式数据到指定队列 +// @Tags RabbitMQ +// @Accept json +// @Produce json +// @Param request body req.SendJSONReq true "发送JSON消息请求" +// @Success 200 {object} controller.ResJson +// @Router /rabbitmq/send-json [post] +func (c *controller) SendJSON() { + var request req.RabbitMQSendJSONReq + if err := c.GCtx.ShouldBindJSON(&request); err != nil { + // logger removed("参数绑定失败: %v", err) + c.Fail400("参数错误: " + err.Error()) + return + } + + service := rabbitmqService.NewRabbitMQService() + if err := service.SendJSON(request.QueueName, request.Data); err != nil { + // logger removed("发送JSON消息失败: %v", err) + c.Fail400(fmt.Sprintf("发送JSON消息失败: %v", err)) + return + } + + c.SuccessWithMsg(resp.RabbitMQMessageResp{ + QueueName: request.QueueName, + Message: fmt.Sprintf("%v", request.Data), + SentAt: time.Now().Format(time.RFC3339), + }, "JSON消息发送成功") +} + +// SendToExchange 发送消息到交换机 +// @Summary 发送消息到交换机 +// @Description 发送消息到指定交换机 +// @Tags RabbitMQ +// @Accept json +// @Produce json +// @Param request body req.SendToExchangeReq true "发送到交换机请求" +// @Success 200 {object} controller.ResJson +// @Router /rabbitmq/send-exchange [post] +func (c *controller) SendToExchange() { + var request req.SendToExchangeReq + if err := c.GCtx.ShouldBindJSON(&request); err != nil { + // logger removed("参数绑定失败: %v", err) + c.Fail400("参数错误: " + err.Error()) + return + } + + service := rabbitmqService.NewRabbitMQService() + if err := service.SendToExchange(request.ExchangeName, request.ExchangeType, request.RoutingKey, request.Message); err != nil { + // logger removed("发送消息到交换机失败: %v", err) + c.Fail400(fmt.Sprintf("发送消息失败: %v", err)) + return + } + + c.SuccessWithMsg(resp.RabbitMQExchangeResp{ + ExchangeName: request.ExchangeName, + ExchangeType: request.ExchangeType, + RoutingKey: request.RoutingKey, + Message: request.Message, + SentAt: time.Now().Format(time.RFC3339), + }, "消息发送成功") +} + +// SendFanout 发送广播消息 +// @Summary 发送广播消息(Fanout模式) +// @Description 发送广播消息到Fanout交换机 +// @Tags RabbitMQ +// @Accept json +// @Produce json +// @Param request body req.SendFanoutReq true "发送广播消息请求" +// @Success 200 {object} controller.ResJson +// @Router /rabbitmq/send-fanout [post] +func (c *controller) SendFanout() { + var request req.SendFanoutReq + if err := c.GCtx.ShouldBindJSON(&request); err != nil { + // logger removed("参数绑定失败: %v", err) + c.Fail400("参数错误: " + err.Error()) + return + } + + service := rabbitmqService.NewRabbitMQService() + if err := service.PublishFanout(request.ExchangeName, request.Message); err != nil { + // logger removed("发送广播消息失败: %v", err) + c.Fail400(fmt.Sprintf("发送消息失败: %v", err)) + return + } + + c.SuccessWithMsg(resp.RabbitMQExchangeResp{ + ExchangeName: request.ExchangeName, + ExchangeType: "fanout", + Message: request.Message, + SentAt: time.Now().Format(time.RFC3339), + }, "广播消息发送成功") +} + +// SendDirect 发送直接消息 +// @Summary 发送直接消息(Direct模式) +// @Description 发送直接消息到Direct交换机 +// @Tags RabbitMQ +// @Accept json +// @Produce json +// @Param request body req.SendDirectReq true "发送直接消息请求" +// @Success 200 {object} controller.ResJson +// @Router /rabbitmq/send-direct [post] +func (c *controller) SendDirect() { + var request req.SendDirectReq + if err := c.GCtx.ShouldBindJSON(&request); err != nil { + // logger removed("参数绑定失败: %v", err) + c.Fail400("参数错误: " + err.Error()) + return + } + + service := rabbitmqService.NewRabbitMQService() + if err := service.PublishDirect(request.ExchangeName, request.RoutingKey, request.Message); err != nil { + // logger removed("发送直接消息失败: %v", err) + c.Fail400(fmt.Sprintf("发送消息失败: %v", err)) + return + } + + c.SuccessWithMsg(resp.RabbitMQExchangeResp{ + ExchangeName: request.ExchangeName, + ExchangeType: "direct", + RoutingKey: request.RoutingKey, + Message: request.Message, + SentAt: time.Now().Format(time.RFC3339), + }, "直接消息发送成功") +} + +// SendTopic 发送主题消息 +// @Summary 发送主题消息(Topic模式) +// @Description 发送主题消息到Topic交换机 +// @Tags RabbitMQ +// @Accept json +// @Produce json +// @Param request body req.SendTopicReq true "发送主题消息请求" +// @Success 200 {object} controller.ResJson +// @Router /rabbitmq/send-topic [post] +func (c *controller) SendTopic() { + var request req.SendTopicReq + if err := c.GCtx.ShouldBindJSON(&request); err != nil { + // logger removed("参数绑定失败: %v", err) + c.Fail400("参数错误: " + err.Error()) + return + } + + service := rabbitmqService.NewRabbitMQService() + if err := service.PublishTopic(request.ExchangeName, request.RoutingKey, request.Message); err != nil { + // logger removed("发送主题消息失败: %v", err) + c.Fail400(fmt.Sprintf("发送消息失败: %v", err)) + return + } + + c.SuccessWithMsg(resp.RabbitMQExchangeResp{ + ExchangeName: request.ExchangeName, + ExchangeType: "topic", + RoutingKey: request.RoutingKey, + Message: request.Message, + SentAt: time.Now().Format(time.RFC3339), + }, "主题消息发送成功") +} + +// SendTask 发送任务消息 +// @Summary 发送任务消息 +// @Description 发送任务消息到队列(Worker模式) +// @Tags RabbitMQ +// @Accept json +// @Produce json +// @Param request body req.SendTaskReq true "发送任务消息请求" +// @Success 200 {object} controller.ResJson +// @Router /rabbitmq/send-task [post] +func (c *controller) SendTask() { + var request req.SendTaskReq + if err := c.GCtx.ShouldBindJSON(&request); err != nil { + // logger removed("参数绑定失败: %v", err) + c.Fail400("参数错误: " + err.Error()) + return + } + + service := rabbitmqService.NewRabbitMQService() + if err := service.SendTask(request.QueueName, request.TaskName, request.TaskData); err != nil { + // logger removed("发送任务消息失败: %v", err) + c.Fail400(fmt.Sprintf("发送任务失败: %v", err)) + return + } + + c.SuccessWithMsg(resp.RabbitMQTaskResp{ + QueueName: request.QueueName, + TaskName: request.TaskName, + Status: "sent", + CreatedAt: time.Now().Format(time.RFC3339), + }, "任务发送成功") +} + +// SendBatch 批量发送消息 +// @Summary 批量发送消息 +// @Description 批量发送消息到队列 +// @Tags RabbitMQ +// @Accept json +// @Produce json +// @Param request body req.SendBatchReq true "批量发送消息请求" +// @Success 200 {object} controller.ResJson +// @Router /rabbitmq/send-batch [post] +func (c *controller) SendBatch() { + var request req.SendBatchReq + if err := c.GCtx.ShouldBindJSON(&request); err != nil { + // logger removed("参数绑定失败: %v", err) + c.Fail400("参数错误: " + err.Error()) + return + } + + service := rabbitmqService.NewRabbitMQService() + if err := service.SendBatch(request.QueueName, request.Messages); err != nil { + // logger removed("批量发送消息失败: %v", err) + c.Fail400(fmt.Sprintf("批量发送失败: %v", err)) + return + } + + c.SuccessWithMsg(resp.RabbitMQBatchResp{ + QueueName: request.QueueName, + TotalCount: len(request.Messages), + SuccessCount: len(request.Messages), + FailedCount: 0, + SentAt: time.Now().Format(time.RFC3339), + }, "批量发送成功") +} + +// HealthCheck 健康检查 +// @Summary RabbitMQ健康检查 +// @Description 检查RabbitMQ连接状态 +// @Tags RabbitMQ +// @Accept json +// @Produce json +// @Success 200 {object} controller.ResJson{data=resp.RabbitMQHealthResp} +// @Router /rabbitmq/health [get] +func (c *controller) HealthCheck() { + service := rabbitmqService.NewRabbitMQService() + if err := service.ValidateConnection(); err != nil { + c.Success(resp.RabbitMQHealthResp{ + Status: "error", + Connected: false, + Message: err.Error(), + }) + return + } + + c.Success(resp.RabbitMQHealthResp{ + Status: "ok", + Connected: true, + Message: "RabbitMQ连接正常", + }) +} + +// ============= 查询相关接口 ============= + +// GetQueueInfo 获取队列信息 +// @Summary 获取队列信息 +// @Description 获取指定队列的详细信息(消息数、消费者数等) +// @Tags RabbitMQ-Query +// @Accept json +// @Produce json +// @Param queue query string true "队列名称" +// @Success 200 {object} controller.ResJson{data=resp.QueueInfoResp} +// @Router /rabbitmq/queue/info [get] +func (c *controller) GetQueueInfo() { + var request req.GetQueueInfoReq + if err := c.GCtx.ShouldBindQuery(&request); err != nil { + // logger removed("参数绑定失败: %v", err) + c.Fail400("参数错误: " + err.Error()) + return + } + + service := rabbitmqService.NewRabbitMQService() + info, err := service.DeclareAndGetQueueInfo(request.Queue) + if err != nil { + // logger removed("获取队列信息失败: %v", err) + c.Fail400(fmt.Sprintf("获取队列信息失败: %v", err)) + return + } + + c.Success(resp.QueueInfoResp{ + Name: info.Name, + Messages: info.Messages, + Consumers: info.Consumers, + Durable: info.Durable, + AutoDelete: info.AutoDelete, + Exclusive: info.Exclusive, + }) +} + +// PeekMessages 查看队列消息(不消费) +// @Summary 查看队列消息 +// @Description 查看队列中的消息,但不从队列中删除(消息会重新入队) +// @Tags RabbitMQ-Query +// @Accept json +// @Produce json +// @Param queue query string true "队列名称" +// @Param limit query int false "查看数量(默认10,最多100)" +// @Success 200 {object} controller.ResJson{data=resp.PeekMessagesResp} +// @Router /rabbitmq/queue/peek [get] +func (c *controller) PeekMessages() { + var request req.PeekMessagesReq + if err := c.GCtx.ShouldBindQuery(&request); err != nil { + // logger removed("参数绑定失败: %v", err) + c.Fail400("参数错误: " + err.Error()) + return + } + + // 设置默认值 + if request.Limit <= 0 { + request.Limit = 10 + } + if request.Limit > 100 { + request.Limit = 100 + } + + service := rabbitmqService.NewRabbitMQService() + messages, err := service.PeekMessages(request.Queue, request.Limit) + if err != nil { + // logger removed("查看消息失败: %v", err) + c.Fail400(fmt.Sprintf("查看消息失败: %v", err)) + return + } + + // 转换消息格式 + var respMessages []resp.QueueMessageResp + for _, msg := range messages { + respMessages = append(respMessages, resp.QueueMessageResp{ + Body: msg.Body, + ContentType: msg.ContentType, + DeliveryMode: msg.DeliveryMode, + Priority: msg.Priority, + CorrelationId: msg.CorrelationId, + ReplyTo: msg.ReplyTo, + Expiration: msg.Expiration, + MessageId: msg.MessageId, + Timestamp: msg.Timestamp.Format(time.RFC3339), + Type: msg.Type, + UserId: msg.UserId, + AppId: msg.AppId, + Headers: msg.Headers, + DeliveryTag: msg.DeliveryTag, + Redelivered: msg.Redelivered, + Exchange: msg.Exchange, + RoutingKey: msg.RoutingKey, + }) + } + + c.Success(resp.PeekMessagesResp{ + Queue: request.Queue, + Total: len(respMessages), + Messages: respMessages, + }) +} + +// ConsumeMessages 消费队列消息 +// @Summary 消费队列消息 +// @Description 从队列中消费消息(消息会被删除) +// @Tags RabbitMQ-Query +// @Accept json +// @Produce json +// @Param queue query string true "队列名称" +// @Param limit query int false "消费数量(默认10,最多100)" +// @Success 200 {object} controller.ResJson{data=resp.ConsumeMessagesResp} +// @Router /rabbitmq/queue/consume [get] +func (c *controller) ConsumeMessages() { + var request req.ConsumeMessagesReq + if err := c.GCtx.ShouldBindQuery(&request); err != nil { + // logger removed("参数绑定失败: %v", err) + c.Fail400("参数错误: " + err.Error()) + return + } + + // 设置默认值 + if request.Limit <= 0 { + request.Limit = 10 + } + if request.Limit > 100 { + request.Limit = 100 + } + + service := rabbitmqService.NewRabbitMQService() + messages, err := service.ConsumeMessages(request.Queue, request.Limit) + if err != nil { + // logger removed("消费消息失败: %v", err) + c.Fail400(fmt.Sprintf("消费消息失败: %v", err)) + return + } + + // 转换消息格式 + var respMessages []resp.QueueMessageResp + for _, msg := range messages { + respMessages = append(respMessages, resp.QueueMessageResp{ + Body: msg.Body, + ContentType: msg.ContentType, + DeliveryMode: msg.DeliveryMode, + Priority: msg.Priority, + CorrelationId: msg.CorrelationId, + ReplyTo: msg.ReplyTo, + Expiration: msg.Expiration, + MessageId: msg.MessageId, + Timestamp: msg.Timestamp.Format(time.RFC3339), + Type: msg.Type, + UserId: msg.UserId, + AppId: msg.AppId, + Headers: msg.Headers, + DeliveryTag: msg.DeliveryTag, + Redelivered: msg.Redelivered, + Exchange: msg.Exchange, + RoutingKey: msg.RoutingKey, + }) + } + + c.Success(resp.ConsumeMessagesResp{ + Queue: request.Queue, + Total: len(respMessages), + Messages: respMessages, + }) +} + +// PurgeQueue 清空队列 +// @Summary 清空队列 +// @Description 删除队列中的所有消息 +// @Tags RabbitMQ-Query +// @Accept json +// @Produce json +// @Param request body req.PurgeQueueReq true "清空队列请求" +// @Success 200 {object} controller.ResJson{data=resp.PurgeQueueResp} +// @Router /rabbitmq/queue/purge [post] +func (c *controller) PurgeQueue() { + var request req.PurgeQueueReq + if err := c.GCtx.ShouldBindJSON(&request); err != nil { + // logger removed("参数绑定失败: %v", err) + c.Fail400("参数错误: " + err.Error()) + return + } + + service := rabbitmqService.NewRabbitMQService() + count, err := service.PurgeQueue(request.Queue) + if err != nil { + // logger removed("清空队列失败: %v", err) + c.Fail400(fmt.Sprintf("清空队列失败: %v", err)) + return + } + + c.SuccessWithMsg(resp.PurgeQueueResp{ + Queue: request.Queue, + DeletedCount: count, + Success: true, + }, fmt.Sprintf("成功清空队列,删除了 %d 条消息", count)) +} + +// DeleteQueue 删除队列 +// @Summary 删除队列 +// @Description 删除指定的队列 +// @Tags RabbitMQ-Query +// @Accept json +// @Produce json +// @Param request body req.DeleteQueueReq true "删除队列请求" +// @Success 200 {object} controller.ResJson{data=resp.DeleteQueueResp} +// @Router /rabbitmq/queue/delete [post] +func (c *controller) DeleteQueue() { + var request req.DeleteQueueReq + if err := c.GCtx.ShouldBindJSON(&request); err != nil { + // logger removed("参数绑定失败: %v", err) + c.Fail400("参数错误: " + err.Error()) + return + } + + service := rabbitmqService.NewRabbitMQService() + count, err := service.DeleteQueue(request.Queue, request.IfUnused, request.IfEmpty) + if err != nil { + // logger removed("删除队列失败: %v", err) + c.Fail400(fmt.Sprintf("删除队列失败: %v", err)) + return + } + + c.SuccessWithMsg(resp.DeleteQueueResp{ + Queue: request.Queue, + DeletedCount: count, + Success: true, + }, fmt.Sprintf("成功删除队列,删除了 %d 条消息", count)) +} diff --git a/internal/app/service/kafkaService/kafkaService.go b/internal/app/service/kafkaService/kafkaService.go new file mode 100644 index 0000000..53e1568 --- /dev/null +++ b/internal/app/service/kafkaService/kafkaService.go @@ -0,0 +1,122 @@ +package kafkaService + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/JasonMetal/simple-develop-template/pkg/support-go/bootstrap" +) + +// KafkaService Kafka服务 +type KafkaService struct{} + +// NewKafkaService 创建Kafka服务 +func NewKafkaService() *KafkaService { + return &KafkaService{} +} + +// SendMessage 发送单条消息(同步) +func (s *KafkaService) SendMessage(topic string, message string) error { + return bootstrap.ProducerSync(topic, message) +} + +// SendMessageAsync 发送单条消息(异步) +func (s *KafkaService) SendMessageAsync(topic string, message string) error { + return bootstrap.ProducerAsync(topic, message) +} + +// SendMessageWithContext 带上下文发送消息 +func (s *KafkaService) SendMessageWithContext(ctx context.Context, topic string, message string) error { + return bootstrap.ProducerSyncWithContext(ctx, topic, message) +} + +// SendJSON 发送JSON格式数据 +func (s *KafkaService) SendJSON(topic string, data interface{}) error { + jsonData, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("JSON序列化失败: %v", err) + } + return s.SendMessage(topic, string(jsonData)) +} + +// SendJSONAsync 异步发送JSON格式数据 +func (s *KafkaService) SendJSONAsync(topic string, data interface{}) error { + jsonData, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("JSON序列化失败: %v", err) + } + return s.SendMessageAsync(topic, string(jsonData)) +} + +// SendBatch 批量发送消息 +func (s *KafkaService) SendBatch(topic string, messages []string) error { + return bootstrap.ProducerSyncBatch(topic, messages) +} + +// SendLog 发送日志消息 +func (s *KafkaService) SendLog(topic string, logLevel string, logMessage string, extra map[string]interface{}) error { + logData := map[string]interface{}{ + "level": logLevel, + "message": logMessage, + "timestamp": time.Now().Format(time.RFC3339), + "extra": extra, + } + return s.SendJSON(topic, logData) +} + +// SendEvent 发送事件消息 +func (s *KafkaService) SendEvent(topic string, eventType string, eventData interface{}) error { + event := map[string]interface{}{ + "event_type": eventType, + "event_data": eventData, + "timestamp": time.Now().Format(time.RFC3339), + } + return s.SendJSON(topic, event) +} + +// SendMetric 发送指标消息 +func (s *KafkaService) SendMetric(topic string, metricName string, metricValue float64, tags map[string]string) error { + metric := map[string]interface{}{ + "metric_name": metricName, + "metric_value": metricValue, + "tags": tags, + "timestamp": time.Now().Unix(), + } + return s.SendJSON(topic, metric) +} + +// FetchMessages 获取指定主题和分区的消息 +func (s *KafkaService) FetchMessages(topic string, partition int32, offset int64, limit int) ([]*bootstrap.KafkaMessage, error) { + if limit <= 0 { + limit = 10 // 默认10条 + } + if limit > 100 { + limit = 100 // 最多100条 + } + + return bootstrap.FetchMessages(topic, partition, offset, limit) +} + +// FetchMessagesFromAllPartitions 从所有分区获取消息 +func (s *KafkaService) FetchMessagesFromAllPartitions(topic string, offset int64, limit int) ([]*bootstrap.KafkaMessage, error) { + if limit <= 0 { + limit = 10 // 默认10条 + } + if limit > 100 { + limit = 100 // 最多100条 + } + + return bootstrap.FetchMessagesFromAllPartitions(topic, offset, limit) +} + +// GetTopicPartitions 获取主题的分区信息 +func (s *KafkaService) GetTopicPartitions(topic string) ([]int32, error) { + return bootstrap.GetTopicPartitions(topic) +} + +// GetPartitionOffset 获取分区的偏移量信息 +func (s *KafkaService) GetPartitionOffset(topic string, partition int32) (oldest int64, newest int64, err error) { + return bootstrap.GetPartitionOffset(topic, partition) +} diff --git a/internal/app/service/rabbitmqService/rabbitmqService.go b/internal/app/service/rabbitmqService/rabbitmqService.go new file mode 100644 index 0000000..dbde88b --- /dev/null +++ b/internal/app/service/rabbitmqService/rabbitmqService.go @@ -0,0 +1,184 @@ +package rabbitmqService + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/JasonMetal/simple-develop-template/pkg/support-go/bootstrap" +) + +// RabbitMQService RabbitMQ服务 +type RabbitMQService struct{} + +// NewRabbitMQService 创建RabbitMQ服务 +func NewRabbitMQService() *RabbitMQService { + return &RabbitMQService{} +} + +// SendMessage 发送单条消息到队列(同步) +func (s *RabbitMQService) SendMessage(queueName string, message string) error { + return bootstrap.PublishSimple(queueName, message) +} + +// SendMessageWithContext 带上下文发送消息 +func (s *RabbitMQService) SendMessageWithContext(ctx context.Context, queueName string, message string) error { + return bootstrap.PublishSimpleWithContext(ctx, queueName, message) +} + +// SendJSON 发送JSON格式数据 +func (s *RabbitMQService) SendJSON(queueName string, data interface{}) error { + return bootstrap.PublishJSON(queueName, data) +} + +// SendJSONWithContext 带上下文发送JSON数据 +func (s *RabbitMQService) SendJSONWithContext(ctx context.Context, queueName string, data interface{}) error { + return bootstrap.PublishJSONWithContext(ctx, queueName, data) +} + +// SendToExchange 发送消息到交换机 +func (s *RabbitMQService) SendToExchange(exchangeName string, exchangeType string, routingKey string, message string) error { + return bootstrap.PublishToExchange(exchangeName, exchangeType, routingKey, message) +} + +// SendToExchangeWithContext 带上下文发送消息到交换机 +func (s *RabbitMQService) SendToExchangeWithContext(ctx context.Context, exchangeName string, exchangeType string, routingKey string, message string) error { + return bootstrap.PublishToExchangeWithContext(ctx, exchangeName, exchangeType, routingKey, message) +} + +// SendLog 发送日志消息 +func (s *RabbitMQService) SendLog(queueName string, logLevel string, logMessage string, extra map[string]interface{}) error { + logData := map[string]interface{}{ + "level": logLevel, + "message": logMessage, + "timestamp": time.Now().Format(time.RFC3339), + "extra": extra, + } + return s.SendJSON(queueName, logData) +} + +// SendEvent 发送事件消息 +func (s *RabbitMQService) SendEvent(queueName string, eventType string, eventData interface{}) error { + event := map[string]interface{}{ + "event_type": eventType, + "event_data": eventData, + "timestamp": time.Now().Format(time.RFC3339), + } + return s.SendJSON(queueName, event) +} + +// PublishFanout 发送广播消息(fanout模式) +func (s *RabbitMQService) PublishFanout(exchangeName string, message string) error { + return s.SendToExchange(exchangeName, "fanout", "", message) +} + +// PublishDirect 发送直接消息(direct模式) +func (s *RabbitMQService) PublishDirect(exchangeName string, routingKey string, message string) error { + return s.SendToExchange(exchangeName, "direct", routingKey, message) +} + +// PublishTopic 发送主题消息(topic模式) +func (s *RabbitMQService) PublishTopic(exchangeName string, routingKey string, message string) error { + return s.SendToExchange(exchangeName, "topic", routingKey, message) +} + +// SendBatch 批量发送消息 +func (s *RabbitMQService) SendBatch(queueName string, messages []string) error { + var errors []error + for i, message := range messages { + if err := s.SendMessage(queueName, message); err != nil { + errors = append(errors, fmt.Errorf("消息 %d 发送失败: %v", i, err)) + } + } + + if len(errors) > 0 { + return fmt.Errorf("批量发送失败,错误数: %d, 首个错误: %v", len(errors), errors[0]) + } + + return nil +} + +// SendJSONBatch 批量发送JSON消息 +func (s *RabbitMQService) SendJSONBatch(queueName string, dataList []interface{}) error { + var errors []error + for i, data := range dataList { + if err := s.SendJSON(queueName, data); err != nil { + errors = append(errors, fmt.Errorf("消息 %d 发送失败: %v", i, err)) + } + } + + if len(errors) > 0 { + return fmt.Errorf("批量发送失败,错误数: %d, 首个错误: %v", len(errors), errors[0]) + } + + return nil +} + +// SendTask 发送任务消息(Worker模式) +func (s *RabbitMQService) SendTask(queueName string, taskName string, taskData interface{}) error { + task := map[string]interface{}{ + "task_name": taskName, + "task_data": taskData, + "created_at": time.Now().Unix(), + "status": "pending", + } + return s.SendJSON(queueName, task) +} + +// SendDelayedMessage 发送延迟消息(需要RabbitMQ延迟插件) +func (s *RabbitMQService) SendDelayedMessage(queueName string, message string, delaySeconds int) error { + // 这需要 rabbitmq-delayed-message-exchange 插件 + // 这里提供基础实现,实际使用需要在bootstrap层添加延迟支持 + return fmt.Errorf("延迟消息功能需要RabbitMQ延迟插件支持") +} + +// ValidateConnection 验证连接状态 +func (s *RabbitMQService) ValidateConnection() error { + manager := bootstrap.GetRabbitMQManager() + if manager == nil { + return fmt.Errorf("RabbitMQ未初始化") + } + return nil +} + +// FormatMessage 格式化消息为JSON字符串 +func (s *RabbitMQService) FormatMessage(data interface{}) (string, error) { + jsonData, err := json.Marshal(data) + if err != nil { + return "", fmt.Errorf("JSON序列化失败: %v", err) + } + return string(jsonData), nil +} + +// ============= 查询相关方法 ============= + +// GetQueueInfo 获取队列信息 +func (s *RabbitMQService) GetQueueInfo(queueName string) (*bootstrap.QueueInfo, error) { + return bootstrap.GetQueueInfo(queueName) +} + +// DeclareAndGetQueueInfo 声明并获取队列信息 +func (s *RabbitMQService) DeclareAndGetQueueInfo(queueName string) (*bootstrap.QueueInfo, error) { + return bootstrap.DeclareAndGetQueueInfo(queueName) +} + +// PeekMessages 查看队列消息(不消费,查看后重新入队) +func (s *RabbitMQService) PeekMessages(queueName string, count int) ([]bootstrap.QueueMessage, error) { + return bootstrap.PeekMessages(queueName, count) +} + +// ConsumeMessages 消费队列消息(会从队列中删除) +func (s *RabbitMQService) ConsumeMessages(queueName string, count int) ([]bootstrap.QueueMessage, error) { + return bootstrap.ConsumeMessages(queueName, count) +} + +// PurgeQueue 清空队列 +func (s *RabbitMQService) PurgeQueue(queueName string) (int, error) { + return bootstrap.PurgeQueue(queueName) +} + +// DeleteQueue 删除队列 +func (s *RabbitMQService) DeleteQueue(queueName string, ifUnused, ifEmpty bool) (int, error) { + return bootstrap.DeleteQueue(queueName, ifUnused, ifEmpty) +} diff --git a/manifest/config/bvt/kafka.yml b/manifest/config/bvt/kafka.yml new file mode 100644 index 0000000..5c7eb68 --- /dev/null +++ b/manifest/config/bvt/kafka.yml @@ -0,0 +1,18 @@ +brokers: + - host: "127.0.0.1" + port: 9092 + +ssl: + enable: false + +producer: + required_acks: 1 + max_retries: 5 + return_successes: true + return_errors: true + +consumer: + group_id: "simple-develop-template" + auto_commit: true + +version: "3.7.2" diff --git a/manifest/config/bvt/rabbitmq.yml b/manifest/config/bvt/rabbitmq.yml new file mode 100644 index 0000000..1e9d92e --- /dev/null +++ b/manifest/config/bvt/rabbitmq.yml @@ -0,0 +1,38 @@ +# RabbitMQ 配置 - BVT 环境 + +host: "rabbitmq-bvt.example.com" +port: 5672 +username: "bvt_user" +password: "bvt_password" +vhost: "/bvt" + +pool: + max_open: 20 + max_idle: 10 + max_lifetime: 7200 + +producer: + confirm_mode: true + mandatory: true + immediate: false + +consumer: + auto_ack: false + prefetch_count: 20 + prefetch_size: 0 + +reconnect: + max_retries: 10 + interval: 3 + +queue: + durable: true + auto_delete: false + exclusive: false + no_wait: false + +exchange: + durable: true + auto_delete: false + internal: false + no_wait: false diff --git a/manifest/config/local/kafka.yml b/manifest/config/local/kafka.yml new file mode 100644 index 0000000..8667c59 --- /dev/null +++ b/manifest/config/local/kafka.yml @@ -0,0 +1,26 @@ +brokers: + - host: "127.0.0.1" + port: 9092 + +# SSL/TLS配置 +ssl: + enable: false + +# 生产者配置 +producer: + # 确认模式: 0-不等待, 1-等待leader, -1-等待所有副本 + required_acks: 1 + # 最大重试次数 + max_retries: 5 + # 返回成功确认 + return_successes: true + # 返回错误信息 + return_errors: true + +# 消费者配置(预留) +consumer: + group_id: "simple-develop-template" + auto_commit: true + +# Kafka版本 +version: "3.7.2" diff --git a/manifest/config/local/rabbitmq.yml b/manifest/config/local/rabbitmq.yml new file mode 100644 index 0000000..d4ef2ba --- /dev/null +++ b/manifest/config/local/rabbitmq.yml @@ -0,0 +1,45 @@ +# RabbitMQ 配置 - 本地环境 + +# 连接配置 +host: "localhost" +port: 5672 +username: "admin" # 使用新创建的用户 +password: "admin123" # 使用新密码 +vhost: "/" + +# 连接池配置 +pool: + max_open: 10 # 最大连接数 + max_idle: 5 # 最大空闲连接数 + max_lifetime: 3600 # 连接最大生命周期(秒) + +# 生产者配置 +producer: + confirm_mode: true # 是否开启消息确认模式 + mandatory: false # 如果为 true,消息无法路由时会返回 + immediate: false # 如果为 true,消息无法立即消费时会返回 + +# 消费者配置 +consumer: + auto_ack: false # 是否自动确认 + prefetch_count: 10 # 预取消息数量 + prefetch_size: 0 # 预取消息大小(0表示不限制) + +# 重连配置 +reconnect: + max_retries: 5 # 最大重试次数 + interval: 5 # 重试间隔(秒) + +# 队列默认配置 +queue: + durable: true # 是否持久化 + auto_delete: false # 是否自动删除 + exclusive: false # 是否排他 + no_wait: false # 是否等待服务器确认 + +# 交换机默认配置 +exchange: + durable: true # 是否持久化 + auto_delete: false # 是否自动删除 + internal: false # 是否内部使用 + no_wait: false # 是否等待服务器确认 diff --git a/manifest/config/prod/kafka.yml b/manifest/config/prod/kafka.yml new file mode 100644 index 0000000..2194c10 --- /dev/null +++ b/manifest/config/prod/kafka.yml @@ -0,0 +1,18 @@ +brokers: + - host: "kafka-prod.example.com" + port: 9092 + +ssl: + enable: true + +producer: + required_acks: -1 + max_retries: 5 + return_successes: true + return_errors: true + +consumer: + group_id: "simple-develop-template" + auto_commit: true + +version: "3.7.2" diff --git a/manifest/config/prod/rabbitmq.yml b/manifest/config/prod/rabbitmq.yml new file mode 100644 index 0000000..db7cf57 --- /dev/null +++ b/manifest/config/prod/rabbitmq.yml @@ -0,0 +1,38 @@ +# RabbitMQ 配置 - 生产环境 + +host: "rabbitmq-prod.example.com" +port: 5672 +username: "prod_user" +password: "prod_password_changeme" +vhost: "/prod" + +pool: + max_open: 50 + max_idle: 25 + max_lifetime: 10800 + +producer: + confirm_mode: true + mandatory: true + immediate: false + +consumer: + auto_ack: false + prefetch_count: 100 + prefetch_size: 0 + +reconnect: + max_retries: 20 + interval: 2 + +queue: + durable: true + auto_delete: false + exclusive: false + no_wait: false + +exchange: + durable: true + auto_delete: false + internal: false + no_wait: false diff --git a/manifest/config/test/kafka.yml b/manifest/config/test/kafka.yml new file mode 100644 index 0000000..8fd73a7 --- /dev/null +++ b/manifest/config/test/kafka.yml @@ -0,0 +1,18 @@ +brokers: + - host: "127.0.0.1" + port: 9092 + +ssl: + enable: true + +producer: + required_acks: -1 + max_retries: 5 + return_successes: true + return_errors: true + +consumer: + group_id: "simple-develop-template" + auto_commit: true + +version: "3.7.2" diff --git a/manifest/config/test/rabbitmq.yml b/manifest/config/test/rabbitmq.yml new file mode 100644 index 0000000..29d55ac --- /dev/null +++ b/manifest/config/test/rabbitmq.yml @@ -0,0 +1,38 @@ +# RabbitMQ 配置 - 测试环境 + +host: "rabbitmq-test.example.com" +port: 5672 +username: "test_user" +password: "test_password" +vhost: "/test" + +pool: + max_open: 30 + max_idle: 15 + max_lifetime: 7200 + +producer: + confirm_mode: true + mandatory: true + immediate: false + +consumer: + auto_ack: false + prefetch_count: 50 + prefetch_size: 0 + +reconnect: + max_retries: 10 + interval: 3 + +queue: + durable: true + auto_delete: false + exclusive: false + no_wait: false + +exchange: + durable: true + auto_delete: false + internal: false + no_wait: false diff --git a/pkg/support-go/bootstrap/app.go b/pkg/support-go/bootstrap/app.go index b16b3e8..c7ca3da 100644 --- a/pkg/support-go/bootstrap/app.go +++ b/pkg/support-go/bootstrap/app.go @@ -61,6 +61,16 @@ func Init() { // InitSts() // InitSms() + + // 初始化Kafka + if err := InitKafka(); err != nil { + logger.Warn("Kafka初始化失败(非致命错误)", zap.Error(err)) + } + + // 初始化RabbitMQ + if err := InitRabbitMQ(); err != nil { + logger.Warn("RabbitMQ初始化失败(非致命错误)", zap.Error(err)) + } } func SetProjectName(name string) { diff --git a/pkg/support-go/bootstrap/kafka.go b/pkg/support-go/bootstrap/kafka.go new file mode 100644 index 0000000..8db6215 --- /dev/null +++ b/pkg/support-go/bootstrap/kafka.go @@ -0,0 +1,655 @@ +package bootstrap + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/IBM/sarama" + "github.com/JasonMetal/simple-develop-template/pkg/support-go/helper/config" +) + +// KafkaConfig Kafka配置结构 +type KafkaConfig struct { + Brokers []BrokerConfig `yaml:"brokers"` + SSL SSLConfig `yaml:"ssl"` + Producer ProducerConfig `yaml:"producer"` + Consumer ConsumerConfig `yaml:"consumer"` + Version string `yaml:"version"` +} + +type BrokerConfig struct { + Host string `yaml:"host"` + Port int `yaml:"port"` +} + +type SSLConfig struct { + Enable bool `yaml:"enable"` +} + +type ProducerConfig struct { + RequiredAcks int `yaml:"required_acks"` + MaxRetries int `yaml:"max_retries"` + ReturnSuccesses bool `yaml:"return_successes"` + ReturnErrors bool `yaml:"return_errors"` +} + +type ConsumerConfig struct { + GroupID string `yaml:"group_id"` + AutoCommit bool `yaml:"auto_commit"` +} + +// KafkaManager Kafka管理器 +type KafkaManager struct { + config *KafkaConfig + brokers []string + syncProducer sarama.SyncProducer + asyncProducer sarama.AsyncProducer +} + +var kafkaManager *KafkaManager + +// InitKafka 初始化Kafka连接 +func InitKafka() error { + configPath := fmt.Sprintf("%smanifest/config/%s/kafka.yml", ProjectPath(), DevEnv) + log.Printf("InitKafka DevEnv %s", DevEnv) + kafkaConfig, err := loadKafkaConfig(configPath) + if err != nil { + return fmt.Errorf("加载Kafka配置失败: %v", err) + } + + // 构建broker地址列表 + brokers := make([]string, 0, len(kafkaConfig.Brokers)) + for _, broker := range kafkaConfig.Brokers { + brokers = append(brokers, fmt.Sprintf("%s:%d", broker.Host, broker.Port)) + } + + kafkaManager = &KafkaManager{ + config: kafkaConfig, + brokers: brokers, + } + + log.Printf("Kafka初始化成功, Brokers: %v", brokers) + return nil +} + +// loadKafkaConfig 加载Kafka配置 +func loadKafkaConfig(configPath string) (*KafkaConfig, error) { + cfg, err := config.GetConfig(configPath) + if err != nil { + return nil, err + } + + kafkaConfig := &KafkaConfig{} + + // 加载brokers + brokersList, err := cfg.List("brokers") + if err != nil { + return nil, fmt.Errorf("读取brokers配置失败: %v", err) + } + + for _, item := range brokersList { + var broker BrokerConfig + var host string + var port int + + // 尝试多种类型转换 + if brokerMap, ok := item.(map[interface{}]interface{}); ok { + host = brokerMap["host"].(string) + // port可能是int或float64 + switch v := brokerMap["port"].(type) { + case int: + port = v + case float64: + port = int(v) + default: + return nil, fmt.Errorf("无法解析broker port: %v", v) + } + } else if brokerMap, ok := item.(map[string]interface{}); ok { + host = brokerMap["host"].(string) + // port可能是int或float64 + switch v := brokerMap["port"].(type) { + case int: + port = v + case float64: + port = int(v) + default: + return nil, fmt.Errorf("无法解析broker port: %v", v) + } + } else { + return nil, fmt.Errorf("无法解析broker配置: %v", item) + } + + broker = BrokerConfig{Host: host, Port: port} + kafkaConfig.Brokers = append(kafkaConfig.Brokers, broker) + } + + // 加载SSL配置 + sslEnable, _ := cfg.Bool("ssl.enable") + kafkaConfig.SSL.Enable = sslEnable + + // 加载Producer配置 + requiredAcks, _ := cfg.Int("producer.required_acks") + maxRetries, _ := cfg.Int("producer.max_retries") + returnSuccesses, _ := cfg.Bool("producer.return_successes") + returnErrors, _ := cfg.Bool("producer.return_errors") + + kafkaConfig.Producer = ProducerConfig{ + RequiredAcks: requiredAcks, + MaxRetries: maxRetries, + ReturnSuccesses: returnSuccesses, + ReturnErrors: returnErrors, + } + + // 加载Consumer配置 + groupID, _ := cfg.String("consumer.group_id") + autoCommit, _ := cfg.Bool("consumer.auto_commit") + + kafkaConfig.Consumer = ConsumerConfig{ + GroupID: groupID, + AutoCommit: autoCommit, + } + + // 加载版本 + version, _ := cfg.String("version") + kafkaConfig.Version = version + + return kafkaConfig, nil +} + +// getProducerConfig 获取生产者配置 +func (km *KafkaManager) getProducerConfig() *sarama.Config { + config := sarama.NewConfig() + + // 设置版本 + if km.config.Version != "" { + if version, err := sarama.ParseKafkaVersion(km.config.Version); err == nil { + config.Version = version + } + } + + // Producer配置 + config.Producer.RequiredAcks = sarama.RequiredAcks(km.config.Producer.RequiredAcks) + config.Producer.Retry.Max = km.config.Producer.MaxRetries + config.Producer.Return.Successes = km.config.Producer.ReturnSuccesses + config.Producer.Return.Errors = km.config.Producer.ReturnErrors + config.Producer.Partitioner = sarama.NewRandomPartitioner + + // SSL配置 + config.Net.TLS.Enable = km.config.SSL.Enable + + return config +} + +// getSyncProducer 获取同步生产者(单例模式) +func (km *KafkaManager) getSyncProducer() (sarama.SyncProducer, error) { + if km.syncProducer != nil { + return km.syncProducer, nil + } + + config := km.getProducerConfig() + producer, err := sarama.NewSyncProducer(km.brokers, config) + if err != nil { + return nil, fmt.Errorf("创建同步生产者失败: %v", err) + } + + km.syncProducer = producer + return producer, nil +} + +type AsyncErrorHandler func(topic string, message string, err error) + +var asyncErrorHandler AsyncErrorHandler + +// SetAsyncErrorHandler 设置异步错误处理器 +func SetAsyncErrorHandler(handler AsyncErrorHandler) { + asyncErrorHandler = handler +} + +// getAsyncProducer 获取异步生产者(单例模式) +func (km *KafkaManager) getAsyncProducer() (sarama.AsyncProducer, error) { + if km.asyncProducer != nil { + return km.asyncProducer, nil + } + + config := km.getProducerConfig() + config.Producer.RequiredAcks = sarama.WaitForLocal // 异步模式使用更高的吞吐量 + + producer, err := sarama.NewAsyncProducer(km.brokers, config) + if err != nil { + return nil, fmt.Errorf("创建异步生产者失败: %v", err) + } + + km.asyncProducer = producer + + // 启动监听goroutine + go func() { + for { + select { + case success := <-producer.Successes(): + if success != nil { + log.Printf("Kafka消息发送成功: Topic=%s, Partition=%d, Offset=%d", + success.Topic, success.Partition, success.Offset) + } + case errMsg := <-producer.Errors(): + if errMsg != nil { + log.Printf("Kafka消息发送失败: %v", errMsg.Err) + + // 调用错误处理器 + if asyncErrorHandler != nil { + asyncErrorHandler( + errMsg.Msg.Topic, + string(errMsg.Msg.Value.(sarama.StringEncoder)), + errMsg.Err, + ) + } + } + } + } + }() + + return producer, nil +} + +// ProducerSync 同步发送消息 +func ProducerSync(topic string, message string) error { + return ProducerSyncWithContext(context.Background(), topic, message) +} + +// ProducerSyncWithContext 带上下文的同步发送消息 +func ProducerSyncWithContext(ctx context.Context, topic string, message string) error { + if kafkaManager == nil { + return fmt.Errorf("Kafka未初始化,请先调用InitKafka") + } + + producer, err := kafkaManager.getSyncProducer() + if err != nil { + return err + } + + msg := &sarama.ProducerMessage{ + Topic: topic, + Value: sarama.StringEncoder(message), + } + + partition, offset, err := producer.SendMessage(msg) + if err != nil { + return fmt.Errorf("发送消息失败: %v", err) + } + + log.Printf("消息发送成功! Topic: %s, Partition: %d, Offset: %d", topic, partition, offset) + return nil +} + +// ProducerAsync 异步发送消息 +func ProducerAsync(topic string, message string) error { + return ProducerAsyncWithContext(context.Background(), topic, message) +} + +// ProducerAsyncWithContext 带上下文的异步发送消息 +func ProducerAsyncWithContext(ctx context.Context, topic string, message string) error { + if kafkaManager == nil { + return fmt.Errorf("Kafka未初始化,请先调用InitKafka") + } + + producer, err := kafkaManager.getAsyncProducer() + if err != nil { + return err + } + + msg := &sarama.ProducerMessage{ + Topic: topic, + Value: sarama.StringEncoder(message), + } + + select { + case producer.Input() <- msg: + return nil + case <-ctx.Done(): + return ctx.Err() + case <-time.After(5 * time.Second): + return fmt.Errorf("发送消息超时") + } +} + +// ProducerSyncBatch 批量同步发送消息 +func ProducerSyncBatch(topic string, messages []string) error { + if kafkaManager == nil { + return fmt.Errorf("Kafka未初始化,请先调用InitKafka") + } + + producer, err := kafkaManager.getSyncProducer() + if err != nil { + return err + } + + var sendErrors []error + for _, message := range messages { + msg := &sarama.ProducerMessage{ + Topic: topic, + Value: sarama.StringEncoder(message), + } + + _, _, err := producer.SendMessage(msg) + if err != nil { + sendErrors = append(sendErrors, err) + } + } + + if len(sendErrors) > 0 { + return fmt.Errorf("批量发送失败, 错误数: %d, 首个错误: %v", len(sendErrors), sendErrors[0]) + } + + log.Printf("批量发送成功! Topic: %s, 消息数: %d", topic, len(messages)) + return nil +} + +// Close 关闭Kafka连接 +func CloseKafka() error { + if kafkaManager == nil { + return nil + } + + var errs []error + + if kafkaManager.syncProducer != nil { + if err := kafkaManager.syncProducer.Close(); err != nil { + errs = append(errs, fmt.Errorf("关闭同步生产者失败: %v", err)) + } + } + + if kafkaManager.asyncProducer != nil { + if err := kafkaManager.asyncProducer.Close(); err != nil { + errs = append(errs, fmt.Errorf("关闭异步生产者失败: %v", err)) + } + } + + if len(errs) > 0 { + return fmt.Errorf("关闭Kafka连接时出现错误: %v", errs) + } + + log.Println("Kafka连接已关闭") + return nil +} + +// GetKafkaManager 获取Kafka管理器实例(用于测试) +func GetKafkaManager() *KafkaManager { + return kafkaManager +} + +// SetKafkaManager 设置Kafka管理器实例(用于测试) +func SetKafkaManager(manager *KafkaManager) { + kafkaManager = manager +} + +// KafkaMessage Kafka消息结构 +type KafkaMessage struct { + Topic string `json:"topic"` + Partition int32 `json:"partition"` + Offset int64 `json:"offset"` + Key string `json:"key"` + Value string `json:"value"` + Timestamp int64 `json:"timestamp"` +} + +// getConsumerConfig 获取消费者配置 +func (km *KafkaManager) getConsumerConfig() *sarama.Config { + config := sarama.NewConfig() + + // 设置版本 + if km.config.Version != "" { + if version, err := sarama.ParseKafkaVersion(km.config.Version); err == nil { + config.Version = version + } + } + + // Consumer配置 + config.Consumer.Return.Errors = true + config.Consumer.Offsets.Initial = sarama.OffsetOldest // 从最早的消息开始读取 + + // SSL配置 + config.Net.TLS.Enable = km.config.SSL.Enable + + return config +} + +// FetchMessages 获取主题消息(支持分页) +func FetchMessages(topic string, partition int32, offset int64, limit int) ([]*KafkaMessage, error) { + if kafkaManager == nil { + return nil, fmt.Errorf("Kafka未初始化,请先调用InitKafka") + } + + config := kafkaManager.getConsumerConfig() + + // 创建 Client 用于获取 offset 信息 + client, err := sarama.NewClient(kafkaManager.brokers, config) + if err != nil { + return nil, fmt.Errorf("创建客户端失败: %v", err) + } + defer client.Close() + + // 创建Consumer + consumer, err := sarama.NewConsumer(kafkaManager.brokers, config) + if err != nil { + return nil, fmt.Errorf("创建消费者失败: %v", err) + } + defer consumer.Close() + + // 获取主题的所有分区 + partitions, err := consumer.Partitions(topic) + if err != nil { + return nil, fmt.Errorf("获取主题分区失败: %v", err) + } + + // 如果没有指定分区,默认使用第一个分区 + if partition < 0 && len(partitions) > 0 { + partition = partitions[0] + } + + // 先检查分区是否有消息可读 + newestOffset, err := client.GetOffset(topic, partition, sarama.OffsetNewest) + if err != nil { + return nil, fmt.Errorf("获取最新偏移量失败: %v", err) + } + + // 如果请求的 offset 已经超过最新 offset,直接返回空结果 + if offset >= newestOffset { + log.Printf("请求的 offset %d 已经到达或超过最新 offset %d,返回空结果", offset, newestOffset) + return []*KafkaMessage{}, nil + } + + // 计算实际可读取的消息数量 + availableMessages := newestOffset - offset + if int64(limit) > availableMessages { + limit = int(availableMessages) + } + + // 如果没有消息可读,直接返回 + if limit <= 0 { + return []*KafkaMessage{}, nil + } + + // 创建分区消费者 + partitionConsumer, err := consumer.ConsumePartition(topic, partition, offset) + if err != nil { + return nil, fmt.Errorf("创建分区消费者失败: %v", err) + } + defer partitionConsumer.Close() + + // 读取消息 + messages := make([]*KafkaMessage, 0, limit) + totalTimeout := time.After(3 * time.Second) // 总超时时间 3 秒 + idleTimeout := time.NewTimer(300 * time.Millisecond) // 空闲超时 300ms + defer idleTimeout.Stop() + + for len(messages) < limit { + select { + case msg := <-partitionConsumer.Messages(): + if msg != nil { + kafkaMsg := &KafkaMessage{ + Topic: msg.Topic, + Partition: msg.Partition, + Offset: msg.Offset, + Key: string(msg.Key), + Value: string(msg.Value), + Timestamp: msg.Timestamp.Unix(), + } + messages = append(messages, kafkaMsg) + + // 重置空闲超时 + if !idleTimeout.Stop() { + select { + case <-idleTimeout.C: + default: + } + } + idleTimeout.Reset(300 * time.Millisecond) + } + case err := <-partitionConsumer.Errors(): + if err != nil { + log.Printf("消费消息错误: %v", err) + } + case <-idleTimeout.C: + // 空闲超时:300ms 内没有新消息,立即返回已读取的消息 + if len(messages) > 0 { + log.Printf("空闲超时,已读取 %d 条消息", len(messages)) + return messages, nil + } + // 如果一条消息都没读到,继续等待 + idleTimeout.Reset(300 * time.Millisecond) + case <-totalTimeout: + // 总超时:3 秒后强制返回 + log.Printf("总超时,已读取 %d 条消息", len(messages)) + return messages, nil + } + } + + return messages, nil +} + +// FetchMessagesFromAllPartitions 从所有分区获取消息 +func FetchMessagesFromAllPartitions(topic string, offset int64, limit int) ([]*KafkaMessage, error) { + if kafkaManager == nil { + return nil, fmt.Errorf("Kafka未初始化,请先调用InitKafka") + } + + config := kafkaManager.getConsumerConfig() + + // 创建Consumer + consumer, err := sarama.NewConsumer(kafkaManager.brokers, config) + if err != nil { + return nil, fmt.Errorf("创建消费者失败: %v", err) + } + defer consumer.Close() + + // 获取主题的所有分区 + partitions, err := consumer.Partitions(topic) + if err != nil { + return nil, fmt.Errorf("获取主题分区失败: %v", err) + } + + if len(partitions) == 0 { + return []*KafkaMessage{}, nil + } + + // 从所有分区读取消息 + messages := make([]*KafkaMessage, 0, limit) + perPartitionLimit := limit / len(partitions) + if perPartitionLimit == 0 { + perPartitionLimit = 1 + } + + for _, partition := range partitions { + partitionConsumer, err := consumer.ConsumePartition(topic, partition, offset) + if err != nil { + log.Printf("创建分区 %d 消费者失败: %v", partition, err) + continue + } + + // 读取该分区的消息 + partitionMessages := 0 + timeout := time.After(2 * time.Second) // 每个分区2秒超时 + + PartitionLoop: + for partitionMessages < perPartitionLimit && len(messages) < limit { + select { + case msg := <-partitionConsumer.Messages(): + if msg != nil { + kafkaMsg := &KafkaMessage{ + Topic: msg.Topic, + Partition: msg.Partition, + Offset: msg.Offset, + Key: string(msg.Key), + Value: string(msg.Value), + Timestamp: msg.Timestamp.Unix(), + } + messages = append(messages, kafkaMsg) + partitionMessages++ + } + case err := <-partitionConsumer.Errors(): + if err != nil { + log.Printf("分区 %d 消费消息错误: %v", partition, err) + } + case <-timeout: + break PartitionLoop + } + } + + partitionConsumer.Close() + } + + return messages, nil +} + +// GetTopicPartitions 获取主题的分区信息 +func GetTopicPartitions(topic string) ([]int32, error) { + if kafkaManager == nil { + return nil, fmt.Errorf("Kafka未初始化,请先调用InitKafka") + } + + config := kafkaManager.getConsumerConfig() + + consumer, err := sarama.NewConsumer(kafkaManager.brokers, config) + if err != nil { + return nil, fmt.Errorf("创建消费者失败: %v", err) + } + defer consumer.Close() + + partitions, err := consumer.Partitions(topic) + if err != nil { + return nil, fmt.Errorf("获取主题分区失败: %v", err) + } + + return partitions, nil +} + +// GetPartitionOffset 获取分区的最新和最早偏移量 +func GetPartitionOffset(topic string, partition int32) (oldest int64, newest int64, err error) { + if kafkaManager == nil { + return 0, 0, fmt.Errorf("Kafka未初始化,请先调用InitKafka") + } + + config := kafkaManager.getConsumerConfig() + + // 使用 Client 而不是 Consumer 来获取 offset + client, err := sarama.NewClient(kafkaManager.brokers, config) + if err != nil { + return 0, 0, fmt.Errorf("创建客户端失败: %v", err) + } + defer client.Close() + + // 获取最早的 offset + oldest, err = client.GetOffset(topic, partition, sarama.OffsetOldest) + if err != nil { + return 0, 0, fmt.Errorf("获取最早偏移量失败: %v", err) + } + + // 获取最新的 offset + newest, err = client.GetOffset(topic, partition, sarama.OffsetNewest) + if err != nil { + return 0, 0, fmt.Errorf("获取最新偏移量失败: %v", err) + } + + return oldest, newest, nil +} diff --git a/pkg/support-go/bootstrap/rabbitmq.go b/pkg/support-go/bootstrap/rabbitmq.go new file mode 100644 index 0000000..8130ffa --- /dev/null +++ b/pkg/support-go/bootstrap/rabbitmq.go @@ -0,0 +1,713 @@ +package bootstrap + +import ( + "context" + "encoding/json" + "fmt" + "log" + "sync" + "time" + + "github.com/JasonMetal/simple-develop-template/pkg/support-go/helper/config" + amqp "github.com/rabbitmq/amqp091-go" +) + +// RabbitMQConfig RabbitMQ配置结构 +type RabbitMQConfig struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + Username string `yaml:"username"` + Password string `yaml:"password"` + Vhost string `yaml:"vhost"` + Pool RabbitMQPoolConfig `yaml:"pool"` + Producer RabbitMQProducerConfig `yaml:"producer"` + Consumer RabbitMQConsumerConfig `yaml:"consumer"` + Reconnect RabbitMQReconnectConfig `yaml:"reconnect"` + Queue RabbitMQQueueConfig `yaml:"queue"` + Exchange RabbitMQExchangeConfig `yaml:"exchange"` +} + +type RabbitMQPoolConfig struct { + MaxOpen int `yaml:"max_open"` + MaxIdle int `yaml:"max_idle"` + MaxLifetime int `yaml:"max_lifetime"` +} + +type RabbitMQProducerConfig struct { + ConfirmMode bool `yaml:"confirm_mode"` + Mandatory bool `yaml:"mandatory"` + Immediate bool `yaml:"immediate"` +} + +type RabbitMQConsumerConfig struct { + AutoAck bool `yaml:"auto_ack"` + PrefetchCount int `yaml:"prefetch_count"` + PrefetchSize int `yaml:"prefetch_size"` +} + +type RabbitMQReconnectConfig struct { + MaxRetries int `yaml:"max_retries"` + Interval int `yaml:"interval"` +} + +type RabbitMQQueueConfig struct { + Durable bool `yaml:"durable"` + AutoDelete bool `yaml:"auto_delete"` + Exclusive bool `yaml:"exclusive"` + NoWait bool `yaml:"no_wait"` +} + +type RabbitMQExchangeConfig struct { + Durable bool `yaml:"durable"` + AutoDelete bool `yaml:"auto_delete"` + Internal bool `yaml:"internal"` + NoWait bool `yaml:"no_wait"` +} + +// RabbitMQManager RabbitMQ管理器 +type RabbitMQManager struct { + config *RabbitMQConfig + conn *amqp.Connection + channel *amqp.Channel + mu sync.RWMutex + closed bool +} + +var ( + rabbitmqManager *RabbitMQManager + rabbitmqOnce sync.Once +) + +// InitRabbitMQ 初始化RabbitMQ连接 +func InitRabbitMQ() error { + configPath := fmt.Sprintf("%smanifest/config/%s/rabbitmq.yml", ProjectPath(), DevEnv) + + log.Printf("Loading RabbitMQ config from: %s", configPath) + + rmqConfig, err := loadRabbitMQConfig(configPath) + if err != nil { + return fmt.Errorf("加载RabbitMQ配置失败: %v", err) + } + + manager := &RabbitMQManager{ + config: rmqConfig, + } + + // 建立连接 + if err := manager.connect(); err != nil { + return fmt.Errorf("连接RabbitMQ失败: %v", err) + } + + rabbitmqManager = manager + + log.Printf("RabbitMQ初始化成功, Host: %s:%d", rmqConfig.Host, rmqConfig.Port) + return nil +} + +// loadRabbitMQConfig 加载RabbitMQ配置 +func loadRabbitMQConfig(configPath string) (*RabbitMQConfig, error) { + cfg, err := config.GetConfig(configPath) + if err != nil { + return nil, err + } + + rmqConfig := &RabbitMQConfig{} + + // 加载基本配置 + rmqConfig.Host, _ = cfg.String("host") + rmqConfig.Port, _ = cfg.Int("port") + rmqConfig.Username, _ = cfg.String("username") + rmqConfig.Password, _ = cfg.String("password") + rmqConfig.Vhost, _ = cfg.String("vhost") + + // 加载连接池配置 + rmqConfig.Pool.MaxOpen, _ = cfg.Int("pool.max_open") + rmqConfig.Pool.MaxIdle, _ = cfg.Int("pool.max_idle") + rmqConfig.Pool.MaxLifetime, _ = cfg.Int("pool.max_lifetime") + + // 加载生产者配置 + rmqConfig.Producer.ConfirmMode, _ = cfg.Bool("producer.confirm_mode") + rmqConfig.Producer.Mandatory, _ = cfg.Bool("producer.mandatory") + rmqConfig.Producer.Immediate, _ = cfg.Bool("producer.immediate") + + // 加载消费者配置 + rmqConfig.Consumer.AutoAck, _ = cfg.Bool("consumer.auto_ack") + rmqConfig.Consumer.PrefetchCount, _ = cfg.Int("consumer.prefetch_count") + rmqConfig.Consumer.PrefetchSize, _ = cfg.Int("consumer.prefetch_size") + + // 加载重连配置 + rmqConfig.Reconnect.MaxRetries, _ = cfg.Int("reconnect.max_retries") + rmqConfig.Reconnect.Interval, _ = cfg.Int("reconnect.interval") + + // 加载队列配置 + rmqConfig.Queue.Durable, _ = cfg.Bool("queue.durable") + rmqConfig.Queue.AutoDelete, _ = cfg.Bool("queue.auto_delete") + rmqConfig.Queue.Exclusive, _ = cfg.Bool("queue.exclusive") + rmqConfig.Queue.NoWait, _ = cfg.Bool("queue.no_wait") + + // 加载交换机配置 + rmqConfig.Exchange.Durable, _ = cfg.Bool("exchange.durable") + rmqConfig.Exchange.AutoDelete, _ = cfg.Bool("exchange.auto_delete") + rmqConfig.Exchange.Internal, _ = cfg.Bool("exchange.internal") + rmqConfig.Exchange.NoWait, _ = cfg.Bool("exchange.no_wait") + + return rmqConfig, nil +} + +// connect 建立连接 +func (m *RabbitMQManager) connect() error { + m.mu.Lock() + defer m.mu.Unlock() + + // 构建连接URL + url := fmt.Sprintf("amqp://%s:%s@%s:%d%s", + m.config.Username, + m.config.Password, + m.config.Host, + m.config.Port, + m.config.Vhost, + ) + + // 建立连接 + conn, err := amqp.Dial(url) + if err != nil { + return fmt.Errorf("连接失败: %v", err) + } + + // 创建Channel + channel, err := conn.Channel() + if err != nil { + conn.Close() + return fmt.Errorf("创建Channel失败: %v", err) + } + + // 设置QoS + if m.config.Consumer.PrefetchCount > 0 { + err = channel.Qos( + m.config.Consumer.PrefetchCount, + m.config.Consumer.PrefetchSize, + false, + ) + if err != nil { + channel.Close() + conn.Close() + return fmt.Errorf("设置QoS失败: %v", err) + } + } + + m.conn = conn + m.channel = channel + m.closed = false + + // 监听连接关闭 + go m.handleReconnect() + + return nil +} + +// handleReconnect 处理重连 +func (m *RabbitMQManager) handleReconnect() { + notifyClose := make(chan *amqp.Error) + m.channel.NotifyClose(notifyClose) + + select { + case err := <-notifyClose: + if err != nil { + log.Printf("RabbitMQ连接断开: %v, 尝试重连...", err) + m.reconnect() + } + } +} + +// reconnect 重新连接 +func (m *RabbitMQManager) reconnect() { + for i := 0; i < m.config.Reconnect.MaxRetries; i++ { + time.Sleep(time.Duration(m.config.Reconnect.Interval) * time.Second) + + log.Printf("RabbitMQ重连尝试 %d/%d", i+1, m.config.Reconnect.MaxRetries) + + if err := m.connect(); err != nil { + log.Printf("重连失败: %v", err) + continue + } + + log.Println("RabbitMQ重连成功") + return + } + + log.Println("RabbitMQ重连失败,已达到最大重试次数") +} + +// PublishSimple 发送简单消息 +func PublishSimple(queueName string, message string) error { + return PublishSimpleWithContext(context.Background(), queueName, message) +} + +// PublishSimpleWithContext 带上下文发送简单消息 +func PublishSimpleWithContext(ctx context.Context, queueName string, message string) error { + if rabbitmqManager == nil { + return fmt.Errorf("RabbitMQ未初始化,请先调用InitRabbitMQ") + } + + rabbitmqManager.mu.RLock() + defer rabbitmqManager.mu.RUnlock() + + if rabbitmqManager.closed { + return fmt.Errorf("RabbitMQ连接已关闭") + } + + // 声明队列 + _, err := rabbitmqManager.channel.QueueDeclare( + queueName, + rabbitmqManager.config.Queue.Durable, + rabbitmqManager.config.Queue.AutoDelete, + rabbitmqManager.config.Queue.Exclusive, + rabbitmqManager.config.Queue.NoWait, + nil, + ) + if err != nil { + return fmt.Errorf("声明队列失败: %v", err) + } + + // 发送消息 + err = rabbitmqManager.channel.PublishWithContext( + ctx, + "", // exchange + queueName, // routing key + rabbitmqManager.config.Producer.Mandatory, + rabbitmqManager.config.Producer.Immediate, + amqp.Publishing{ + DeliveryMode: amqp.Persistent, + ContentType: "text/plain", + Body: []byte(message), + Timestamp: time.Now(), + }, + ) + if err != nil { + return fmt.Errorf("发送消息失败: %v", err) + } + + log.Printf("消息发送成功! Queue: %s", queueName) + return nil +} + +// PublishJSON 发送JSON消息 +func PublishJSON(queueName string, data interface{}) error { + return PublishJSONWithContext(context.Background(), queueName, data) +} + +// PublishJSONWithContext 带上下文发送JSON消息 +func PublishJSONWithContext(ctx context.Context, queueName string, data interface{}) error { + jsonData, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("JSON序列化失败: %v", err) + } + + if rabbitmqManager == nil { + return fmt.Errorf("RabbitMQ未初始化,请先调用InitRabbitMQ") + } + + rabbitmqManager.mu.RLock() + defer rabbitmqManager.mu.RUnlock() + + if rabbitmqManager.closed { + return fmt.Errorf("RabbitMQ连接已关闭") + } + + // 声明队列 + _, err = rabbitmqManager.channel.QueueDeclare( + queueName, + rabbitmqManager.config.Queue.Durable, + rabbitmqManager.config.Queue.AutoDelete, + rabbitmqManager.config.Queue.Exclusive, + rabbitmqManager.config.Queue.NoWait, + nil, + ) + if err != nil { + return fmt.Errorf("声明队列失败: %v", err) + } + + // 发送消息 + err = rabbitmqManager.channel.PublishWithContext( + ctx, + "", // exchange + queueName, // routing key + rabbitmqManager.config.Producer.Mandatory, + rabbitmqManager.config.Producer.Immediate, + amqp.Publishing{ + DeliveryMode: amqp.Persistent, + ContentType: "application/json", + Body: jsonData, + Timestamp: time.Now(), + }, + ) + if err != nil { + return fmt.Errorf("发送消息失败: %v", err) + } + + log.Printf("JSON消息发送成功! Queue: %s", queueName) + return nil +} + +// PublishToExchange 发送消息到交换机 +func PublishToExchange(exchangeName string, exchangeType string, routingKey string, message string) error { + return PublishToExchangeWithContext(context.Background(), exchangeName, exchangeType, routingKey, message) +} + +// PublishToExchangeWithContext 带上下文发送消息到交换机 +func PublishToExchangeWithContext(ctx context.Context, exchangeName string, exchangeType string, routingKey string, message string) error { + if rabbitmqManager == nil { + return fmt.Errorf("RabbitMQ未初始化,请先调用InitRabbitMQ") + } + + rabbitmqManager.mu.RLock() + defer rabbitmqManager.mu.RUnlock() + + if rabbitmqManager.closed { + return fmt.Errorf("RabbitMQ连接已关闭") + } + + // 声明交换机 + err := rabbitmqManager.channel.ExchangeDeclare( + exchangeName, + exchangeType, // direct, fanout, topic, headers + rabbitmqManager.config.Exchange.Durable, + rabbitmqManager.config.Exchange.AutoDelete, + rabbitmqManager.config.Exchange.Internal, + rabbitmqManager.config.Exchange.NoWait, + nil, + ) + if err != nil { + return fmt.Errorf("声明交换机失败: %v", err) + } + + // 发送消息 + err = rabbitmqManager.channel.PublishWithContext( + ctx, + exchangeName, // exchange + routingKey, // routing key + rabbitmqManager.config.Producer.Mandatory, + rabbitmqManager.config.Producer.Immediate, + amqp.Publishing{ + DeliveryMode: amqp.Persistent, + ContentType: "text/plain", + Body: []byte(message), + Timestamp: time.Now(), + }, + ) + if err != nil { + return fmt.Errorf("发送消息失败: %v", err) + } + + log.Printf("消息发送成功! Exchange: %s, RoutingKey: %s", exchangeName, routingKey) + return nil +} + +// CloseRabbitMQ 关闭RabbitMQ连接 +func CloseRabbitMQ() error { + if rabbitmqManager == nil { + return nil + } + + rabbitmqManager.mu.Lock() + defer rabbitmqManager.mu.Unlock() + + rabbitmqManager.closed = true + + if rabbitmqManager.channel != nil { + if err := rabbitmqManager.channel.Close(); err != nil { + log.Printf("关闭Channel失败: %v", err) + } + } + + if rabbitmqManager.conn != nil { + if err := rabbitmqManager.conn.Close(); err != nil { + log.Printf("关闭连接失败: %v", err) + } + } + + log.Println("RabbitMQ连接已关闭") + return nil +} + +// GetRabbitMQManager 获取RabbitMQ管理器实例(用于测试) +func GetRabbitMQManager() *RabbitMQManager { + return rabbitmqManager +} + +// ============= 查询相关功能 ============= + +// QueueInfo 队列信息 +type QueueInfo struct { + Name string `json:"name"` + Messages int `json:"messages"` // 队列中的消息数 + Consumers int `json:"consumers"` // 消费者数量 + Durable bool `json:"durable"` // 是否持久化 + AutoDelete bool `json:"auto_delete"` // 是否自动删除 + Exclusive bool `json:"exclusive"` // 是否排他 +} + +// QueueMessage 队列消息 +type QueueMessage struct { + Body string `json:"body"` // 消息体 + ContentType string `json:"content_type"` // 内容类型 + DeliveryMode uint8 `json:"delivery_mode"` // 投递模式:1=非持久化, 2=持久化 + Priority uint8 `json:"priority"` // 优先级 + CorrelationId string `json:"correlation_id"` // 关联ID + ReplyTo string `json:"reply_to"` // 回复队列 + Expiration string `json:"expiration"` // 过期时间 + MessageId string `json:"message_id"` // 消息ID + Timestamp time.Time `json:"timestamp"` // 时间戳 + Type string `json:"type"` // 消息类型 + UserId string `json:"user_id"` // 用户ID + AppId string `json:"app_id"` // 应用ID + Headers map[string]string `json:"headers"` // 消息头 + DeliveryTag uint64 `json:"delivery_tag"` // 投递标签 + Redelivered bool `json:"redelivered"` // 是否重新投递 + Exchange string `json:"exchange"` // 交换机 + RoutingKey string `json:"routing_key"` // 路由键 +} + +// GetQueueInfo 获取指定队列的详细信息 +func GetQueueInfo(queueName string) (*QueueInfo, error) { + if rabbitmqManager == nil { + return nil, fmt.Errorf("RabbitMQ未初始化") + } + + rabbitmqManager.mu.RLock() + defer rabbitmqManager.mu.RUnlock() + + if rabbitmqManager.closed { + return nil, fmt.Errorf("RabbitMQ连接已关闭") + } + + // 使用 QueueInspect 被动声明队列以获取信息 + queue, err := rabbitmqManager.channel.QueueInspect(queueName) + if err != nil { + return nil, fmt.Errorf("获取队列信息失败: %v", err) + } + + return &QueueInfo{ + Name: queue.Name, + Messages: queue.Messages, + Consumers: queue.Consumers, + }, nil +} + +// DeclareAndGetQueueInfo 声明并获取队列信息(如果队列不存在则创建) +func DeclareAndGetQueueInfo(queueName string) (*QueueInfo, error) { + if rabbitmqManager == nil { + return nil, fmt.Errorf("RabbitMQ未初始化") + } + + rabbitmqManager.mu.RLock() + defer rabbitmqManager.mu.RUnlock() + + if rabbitmqManager.closed { + return nil, fmt.Errorf("RabbitMQ连接已关闭") + } + + // 声明队列 + queue, err := rabbitmqManager.channel.QueueDeclare( + queueName, // 队列名称 + rabbitmqManager.config.Queue.Durable, // 持久化 + rabbitmqManager.config.Queue.AutoDelete, // 自动删除 + rabbitmqManager.config.Queue.Exclusive, // 排他 + rabbitmqManager.config.Queue.NoWait, // 不等待 + nil, // 参数 + ) + if err != nil { + return nil, fmt.Errorf("声明队列失败: %v", err) + } + + return &QueueInfo{ + Name: queue.Name, + Messages: queue.Messages, + Consumers: queue.Consumers, + Durable: rabbitmqManager.config.Queue.Durable, + AutoDelete: rabbitmqManager.config.Queue.AutoDelete, + Exclusive: rabbitmqManager.config.Queue.Exclusive, + }, nil +} + +// PeekMessages 查看队列消息(不消费,查看后重新入队) +// queueName: 队列名称 +// count: 要查看的消息数量(最多100条) +func PeekMessages(queueName string, count int) ([]QueueMessage, error) { + if rabbitmqManager == nil { + return nil, fmt.Errorf("RabbitMQ未初始化") + } + + if count <= 0 || count > 100 { + return nil, fmt.Errorf("count 必须在 1-100 之间") + } + + rabbitmqManager.mu.RLock() + defer rabbitmqManager.mu.RUnlock() + + if rabbitmqManager.closed { + return nil, fmt.Errorf("RabbitMQ连接已关闭") + } + + var messages []QueueMessage + + for i := 0; i < count; i++ { + // 使用 Get 方法获取单条消息(autoAck=false,不自动确认) + delivery, ok, err := rabbitmqManager.channel.Get(queueName, false) + if err != nil { + return messages, fmt.Errorf("获取消息失败: %v", err) + } + + // 没有更多消息 + if !ok { + break + } + + // 转换消息头 + headers := make(map[string]string) + if delivery.Headers != nil { + for k, v := range delivery.Headers { + headers[k] = fmt.Sprintf("%v", v) + } + } + + msg := QueueMessage{ + Body: string(delivery.Body), + ContentType: delivery.ContentType, + DeliveryMode: delivery.DeliveryMode, + Priority: delivery.Priority, + CorrelationId: delivery.CorrelationId, + ReplyTo: delivery.ReplyTo, + Expiration: delivery.Expiration, + MessageId: delivery.MessageId, + Timestamp: delivery.Timestamp, + Type: delivery.Type, + UserId: delivery.UserId, + AppId: delivery.AppId, + Headers: headers, + DeliveryTag: delivery.DeliveryTag, + Redelivered: delivery.Redelivered, + Exchange: delivery.Exchange, + RoutingKey: delivery.RoutingKey, + } + + messages = append(messages, msg) + + // Nack 并重新入队,这样消息不会丢失(查看模式) + err = rabbitmqManager.channel.Nack(delivery.DeliveryTag, false, true) + if err != nil { + log.Printf("拒绝消息失败: %v", err) + } + } + + return messages, nil +} + +// ConsumeMessages 消费队列消息(会从队列中删除) +// queueName: 队列名称 +// count: 要消费的消息数量(最多100条) +func ConsumeMessages(queueName string, count int) ([]QueueMessage, error) { + if rabbitmqManager == nil { + return nil, fmt.Errorf("RabbitMQ未初始化") + } + + if count <= 0 || count > 100 { + return nil, fmt.Errorf("count 必须在 1-100 之间") + } + + rabbitmqManager.mu.RLock() + defer rabbitmqManager.mu.RUnlock() + + if rabbitmqManager.closed { + return nil, fmt.Errorf("RabbitMQ连接已关闭") + } + + var messages []QueueMessage + + for i := 0; i < count; i++ { + // 使用 Get 方法获取单条消息(autoAck=true,自动确认并删除) + delivery, ok, err := rabbitmqManager.channel.Get(queueName, true) + if err != nil { + return messages, fmt.Errorf("获取消息失败: %v", err) + } + + // 没有更多消息 + if !ok { + break + } + + // 转换消息头 + headers := make(map[string]string) + if delivery.Headers != nil { + for k, v := range delivery.Headers { + headers[k] = fmt.Sprintf("%v", v) + } + } + + msg := QueueMessage{ + Body: string(delivery.Body), + ContentType: delivery.ContentType, + DeliveryMode: delivery.DeliveryMode, + Priority: delivery.Priority, + CorrelationId: delivery.CorrelationId, + ReplyTo: delivery.ReplyTo, + Expiration: delivery.Expiration, + MessageId: delivery.MessageId, + Timestamp: delivery.Timestamp, + Type: delivery.Type, + UserId: delivery.UserId, + AppId: delivery.AppId, + Headers: headers, + DeliveryTag: delivery.DeliveryTag, + Redelivered: delivery.Redelivered, + Exchange: delivery.Exchange, + RoutingKey: delivery.RoutingKey, + } + + messages = append(messages, msg) + } + + return messages, nil +} + +// PurgeQueue 清空队列中的所有消息 +func PurgeQueue(queueName string) (int, error) { + if rabbitmqManager == nil { + return 0, fmt.Errorf("RabbitMQ未初始化") + } + + rabbitmqManager.mu.RLock() + defer rabbitmqManager.mu.RUnlock() + + if rabbitmqManager.closed { + return 0, fmt.Errorf("RabbitMQ连接已关闭") + } + + count, err := rabbitmqManager.channel.QueuePurge(queueName, false) + if err != nil { + return 0, fmt.Errorf("清空队列失败: %v", err) + } + + return count, nil +} + +// DeleteQueue 删除队列 +func DeleteQueue(queueName string, ifUnused, ifEmpty bool) (int, error) { + if rabbitmqManager == nil { + return 0, fmt.Errorf("RabbitMQ未初始化") + } + + rabbitmqManager.mu.RLock() + defer rabbitmqManager.mu.RUnlock() + + if rabbitmqManager.closed { + return 0, fmt.Errorf("RabbitMQ连接已关闭") + } + + count, err := rabbitmqManager.channel.QueueDelete(queueName, ifUnused, ifEmpty, false) + if err != nil { + return 0, fmt.Errorf("删除队列失败: %v", err) + } + + return count, nil +} diff --git a/routes/api/kafkaRouter/kafka.go b/routes/api/kafkaRouter/kafka.go new file mode 100644 index 0000000..b6507fe --- /dev/null +++ b/routes/api/kafkaRouter/kafka.go @@ -0,0 +1,29 @@ +package kafkaRouter + +import ( + "github.com/JasonMetal/simple-develop-template/internal/app/http/controller/api/kafkaController" + "github.com/gin-gonic/gin" +) + +func RegisterKafka(router *gin.Engine) { + apiGroup := router.Group("kafka") + + // Kafka消息发送接口 + apiGroup.POST("send", func(ctx *gin.Context) { + kafkaController.NewController(ctx).SendMessage() + }) + + apiGroup.POST("send-json", func(ctx *gin.Context) { + kafkaController.NewController(ctx).SendJSON() + }) + + // Kafka消息查询接口 + apiGroup.GET("messages", func(ctx *gin.Context) { + kafkaController.NewController(ctx).FetchMessages() + }) + + // Kafka主题信息接口 + apiGroup.GET("topic-info", func(ctx *gin.Context) { + kafkaController.NewController(ctx).GetTopicInfo() + }) +} diff --git a/routes/api/rabbitmqRouter/rabbitmq.go b/routes/api/rabbitmqRouter/rabbitmq.go new file mode 100644 index 0000000..80778e8 --- /dev/null +++ b/routes/api/rabbitmqRouter/rabbitmq.go @@ -0,0 +1,75 @@ +package rabbitmqRouter + +import ( + "github.com/JasonMetal/simple-develop-template/internal/app/http/controller/api/rabbitmqController" + "github.com/gin-gonic/gin" +) + +func RegisterRabbitMQ(router *gin.Engine) { + // RabbitMQ基础消息发送接口 + router.POST("/rabbitmq/send", func(ctx *gin.Context) { + rabbitmqController.NewController(ctx).SendMessage() + }) + + router.POST("/rabbitmq/send-json", func(ctx *gin.Context) { + rabbitmqController.NewController(ctx).SendJSON() + }) + + // RabbitMQ交换机模式接口 + router.POST("/rabbitmq/send-exchange", func(ctx *gin.Context) { + rabbitmqController.NewController(ctx).SendToExchange() + }) + + router.POST("/rabbitmq/send-fanout", func(ctx *gin.Context) { + rabbitmqController.NewController(ctx).SendFanout() + }) + + router.POST("/rabbitmq/send-direct", func(ctx *gin.Context) { + rabbitmqController.NewController(ctx).SendDirect() + }) + + router.POST("/rabbitmq/send-topic", func(ctx *gin.Context) { + rabbitmqController.NewController(ctx).SendTopic() + }) + + // RabbitMQ任务和批量接口 + router.POST("/rabbitmq/send-task", func(ctx *gin.Context) { + rabbitmqController.NewController(ctx).SendTask() + }) + + router.POST("/rabbitmq/send-batch", func(ctx *gin.Context) { + rabbitmqController.NewController(ctx).SendBatch() + }) + + // RabbitMQ健康检查接口 + router.GET("/rabbitmq/health", func(ctx *gin.Context) { + rabbitmqController.NewController(ctx).HealthCheck() + }) + + // ============= RabbitMQ查询接口 ============= + + // 获取队列信息 + router.GET("/rabbitmq/queue/info", func(ctx *gin.Context) { + rabbitmqController.NewController(ctx).GetQueueInfo() + }) + + // 查看队列消息(不消费) + router.GET("/rabbitmq/queue/peek", func(ctx *gin.Context) { + rabbitmqController.NewController(ctx).PeekMessages() + }) + + // 消费队列消息(会删除) + router.GET("/rabbitmq/queue/consume", func(ctx *gin.Context) { + rabbitmqController.NewController(ctx).ConsumeMessages() + }) + + // 清空队列 + router.POST("/rabbitmq/queue/purge", func(ctx *gin.Context) { + rabbitmqController.NewController(ctx).PurgeQueue() + }) + + // 删除队列 + router.POST("/rabbitmq/queue/delete", func(ctx *gin.Context) { + rabbitmqController.NewController(ctx).DeleteQueue() + }) +} diff --git a/routes/base.go b/routes/base.go index 6d2706d..c04df28 100644 --- a/routes/base.go +++ b/routes/base.go @@ -2,6 +2,8 @@ package router import ( "github.com/JasonMetal/simple-develop-template/routes/api/helloRouter" + "github.com/JasonMetal/simple-develop-template/routes/api/kafkaRouter" + "github.com/JasonMetal/simple-develop-template/routes/api/rabbitmqRouter" "github.com/gin-gonic/gin" "strings" ) @@ -15,6 +17,8 @@ func RegisterConfig() []func(r *gin.Engine) { // 功能模块路由 helloRouter.RegisterHello, + kafkaRouter.RegisterKafka, + rabbitmqRouter.RegisterRabbitMQ, } }