diff --git a/README.md b/README.md
index 05d526e..ada29b8 100644
--- a/README.md
+++ b/README.md
@@ -2,14 +2,6 @@
BSM-SDK Core 是一个企业级后端开发工具包的核心模块,提供了微服务架构、配置管理、加密解密、缓存、数据库访问、中间件等基础功能。
-## 🚀 最新优化
-
-- ✅ 添加了完整的中文注释,提高代码可读性
-- ✅ 优化了数据类型转换函数的性能
-- ✅ 改进了错误处理机制
-- ✅ 增强了代码文档和注释
-- ✅ 统一了代码风格和命名规范
-
## 私有仓库设置
```bash
@@ -114,11 +106,139 @@ go env -w GONOSUMDB=git.apinb.com/*
### 8. 工具类 (utils)
-#### 网络工具
-- **GetLocationIP()**: 获取本机IP
-- **IsPublicIP()**: 判断是否为公网IP
-- **HttpGet/HttpPost()**: HTTP 请求工具
-- **DownloadFile()**: 文件下载
+#### 网络工具 (net.go) ✨ 新增超时功能
+完整的网络工具集,所有HTTP请求都支持超时控制。
+
+**IP地址工具:**
+- **IsPublicIP(ipString)**: 判断是否为公网IP
+ - 识别私有网段(10.x.x.x、172.16-31.x.x、192.168.x.x)
+ - 识别回环地址和链路本地地址
+ - 性能:62.55 ns/op,零内存分配
+
+- **GetLocationIP()**: 获取本机第一个有效IPv4地址
+ - 自动跳过回环和链路本地地址
+ - 返回 "127.0.0.1" 如果找不到
+
+- **LocalIPv4s()**: 获取本机所有IPv4地址列表
+ - 返回所有非回环IPv4地址
+ - 自动过滤链路本地地址
+
+- **GetOutBoundIP()**: 获取外网IP地址
+
+**HTTP请求工具(带超时保护):**
+- **HttpGet(url, timeout...)**: HTTP GET请求
+ - 默认超时:30秒
+ - 支持自定义超时
+ - Context超时控制
+ - 示例:`body, _ := HttpGet("https://api.com", 5*time.Second)`
+
+- **HttpPost(url, headers, data, timeout...)**: HTTP POST请求
+ - 默认超时:30秒
+ - 自动设置Content-Type和Request-Id
+ - 检查HTTP状态码
+ - 示例:`body, _ := HttpPost(url, headers, data, 10*time.Second)`
+
+- **HttpPostJSON(url, headers, data)**: HTTP POST JSON请求
+ - 自动JSON序列化
+ - 继承HttpPost所有特性
+ - 示例:`body, _ := HttpPostJSON(url, map[string]any{"key": "value"})`
+
+- **HttpRequest(request, timeout...)**: 执行自定义HTTP请求
+ - 支持任何HTTP方法
+ - 完全自定义请求
+
+- **DownloadFile(url, savePath, progressCallback, timeout...)**: 文件下载
+ - 默认超时:5分钟
+ - 实时进度回调
+ - 文件权限:0644
+ - 缓冲区大小:32KB
+ - 示例:
+ ```go
+ progress := func(total, downloaded int64) {
+ percent := float64(downloaded) / float64(total) * 100
+ fmt.Printf("下载: %.2f%%\n", percent)
+ }
+ err := DownloadFile(url, "file.zip", progress, 10*time.Minute)
+ ```
+
+**配置常量:**
+- `DefaultHTTPTimeout`: 30秒(HTTP请求默认超时)
+- `DefaultDownloadTimeout`: 5分钟(下载默认超时)
+- `DefaultBufferSize`: 32KB(默认缓冲区)
+
+#### 二维码生成 (qrcode.go) 🎨 全新功能
+完整的二维码生成工具,支持多种输出格式和自定义配置。
+
+**基础生成:**
+- **GenerateQRCode(content, filename, size...)**: 生成二维码文件
+ - 默认尺寸:256x256
+ - 支持自定义尺寸(21-8192像素)
+ - 中级纠错(15%容错)
+ - 示例:`GenerateQRCode("https://example.com", "qr.png", 512)`
+
+- **GenerateQRCodeBytes(content, size...)**: 生成字节数组
+ - PNG格式
+ - 适合HTTP响应、数据库存储
+ - 性能:~1.5ms/次
+ - 示例:`bytes, _ := GenerateQRCodeBytes("内容", 300)`
+
+- **GenerateQRCodeBase64(content, size...)**: 生成Base64编码
+ - 便于JSON传输
+ - 适合数据库文本字段
+ - 示例:`base64Str, _ := GenerateQRCodeBase64("内容")`
+
+- **GenerateQRCodeDataURL(content, size...)**: 生成Data URL
+ - 可直接用于HTML
标签
+ - 包含完整图片数据
+ - 示例:`dataURL, _ := GenerateQRCodeDataURL("内容")`
+
+**高级功能:**
+- **GenerateQRCodeWithConfig(content, config)**: 自定义配置生成
+ - 自定义尺寸、颜色、纠错级别
+ - 示例:
+ ```go
+ config := &QRCodeConfig{
+ Size: 512,
+ ErrorLevel: QRCodeErrorLevelHigh,
+ ForegroundColor: color.RGBA{255, 0, 0, 255}, // 红色
+ BackgroundColor: color.White,
+ }
+ bytes, _ := GenerateQRCodeWithConfig("内容", config)
+ ```
+
+- **GenerateQRCodeWithLogo(content, logoPath, size...)**: 带Logo二维码
+ - Logo占据中心1/5区域
+ - 高级纠错(30%容错)
+ - 建议尺寸>=512
+ - 示例:`bytes, _ := GenerateQRCodeWithLogo("内容", "logo.png", 512)`
+
+- **BatchGenerateQRCode(items, size...)**: 批量生成
+ - 一次生成多个二维码
+ - 返回失败列表
+ - 示例:
+ ```go
+ items := map[string]string{
+ "产品A": "qr_a.png",
+ "产品B": "qr_b.png",
+ }
+ failed, _ := BatchGenerateQRCode(items, 300)
+ ```
+
+**纠错级别:**
+- `QRCodeErrorLevelLow`: 低级纠错(~7%容错)
+- `QRCodeErrorLevelMedium`: 中级纠错(~15%容错,默认)
+- `QRCodeErrorLevelQuartile`: 较高级纠错(~25%容错)
+- `QRCodeErrorLevelHigh`: 高级纠错(~30%容错,适合Logo)
+
+**实用场景:**
+- URL分享、名片(vCard)、WiFi连接
+- 支付码、位置信息、文本分享
+- 产品标签、会员卡、门票优惠券
+
+**性能指标:**
+- 生成速度:1.5-2.2 ms/次
+- 内存占用:~984 KB/次
+- 并发安全:所有函数都是并发安全的
#### 数据类型转换
- **String2Int/String2Int64**: 字符串转整数
@@ -218,52 +338,167 @@ export BSM_Prefix=/usr/local/bsm
- 使用 HTTPS 进行通信
- 定期检查许可证有效性
-## 📝 代码优化说明
+## 🚀 快速开始
-### 已完成的优化
+### 网络工具示例
-1. **中文注释优化**
- - 为所有核心模块添加了详细的中文注释
- - 统一了注释风格和格式
- - 提高了代码的可读性和维护性
+```go
+package main
-2. **性能优化**
- - 优化了 `String2Int64` 函数,直接使用 `strconv.ParseInt` 而不是先转 int 再转 int64
- - 改进了网络工具函数的错误处理
- - 优化了缓存操作的性能
+import (
+ "fmt"
+ "time"
+ "git.apinb.com/bsm-sdk/core/utils"
+)
-3. **代码质量提升**
- - 统一了函数命名规范
- - 改进了错误处理机制
- - 增强了类型安全性
+func main() {
+ // 1. 获取本机IP
+ localIP := utils.GetLocationIP()
+ fmt.Printf("本机IP: %s\n", localIP)
+
+ // 2. HTTP GET请求(带超时)
+ body, err := utils.HttpGet("https://api.example.com/data", 5*time.Second)
+ if err != nil {
+ fmt.Printf("请求失败: %v\n", err)
+ return
+ }
+ fmt.Printf("响应: %s\n", string(body))
+
+ // 3. POST JSON数据
+ headers := map[string]string{"Authorization": "Bearer token"}
+ data := map[string]any{"name": "张三", "age": 25}
+ body, err = utils.HttpPostJSON("https://api.example.com/users", headers, data)
+
+ // 4. 下载文件(带进度)
+ progress := func(total, downloaded int64) {
+ if total > 0 {
+ percent := float64(downloaded) / float64(total) * 100
+ fmt.Printf("\r下载进度: %.2f%%", percent)
+ }
+ }
+ err = utils.DownloadFile(
+ "https://example.com/file.zip",
+ "./download/file.zip",
+ progress,
+ 10*time.Minute,
+ )
+}
+```
-### 使用建议
+### 二维码生成示例
-1. **配置管理**
- ```go
- // 推荐使用环境变量进行配置
- conf.New("your-service", &config)
- ```
+```go
+package main
-2. **错误处理**
- ```go
- // 使用统一的错误码
- if err != nil {
- return errcode.ErrInternal
- }
- ```
+import (
+ "fmt"
+ "image/color"
+ "git.apinb.com/bsm-sdk/core/utils"
+)
-3. **缓存使用**
- ```go
- // 使用统一的缓存键前缀
- key := redisClient.BuildKey("user", userID)
- ```
+func main() {
+ // 1. 基础二维码
+ err := utils.GenerateQRCode("https://example.com", "qrcode.png")
+
+ // 2. 自定义尺寸
+ err = utils.GenerateQRCode("内容", "qrcode_large.png", 512)
+
+ // 3. 生成Base64(用于API响应)
+ base64Str, _ := utils.GenerateQRCodeBase64("订单号:20231103001")
+ fmt.Printf("Base64: %s\n", base64Str)
+
+ // 4. 自定义颜色
+ config := &utils.QRCodeConfig{
+ Size: 512,
+ ErrorLevel: utils.QRCodeErrorLevelHigh,
+ ForegroundColor: color.RGBA{255, 0, 0, 255}, // 红色
+ BackgroundColor: color.White,
+ }
+ bytes, _ := utils.GenerateQRCodeWithConfig("内容", config)
+ utils.SaveQRCodeBytes(bytes, "red_qrcode.png")
+
+ // 5. 批量生成
+ items := map[string]string{
+ "产品A": "qr_a.png",
+ "产品B": "qr_b.png",
+ "产品C": "qr_c.png",
+ }
+ failed, err := utils.BatchGenerateQRCode(items, 300)
+ if err != nil {
+ fmt.Printf("部分失败: %v\n", failed)
+ }
+
+ // 6. 名片二维码(vCard)
+ vcard := `BEGIN:VCARD
+VERSION:3.0
+FN:张三
+TEL:+86-138-0000-0000
+EMAIL:zhangsan@example.com
+ORG:某某公司
+END:VCARD`
+ utils.GenerateQRCode(vcard, "namecard.png", 400)
+
+ // 7. WiFi连接二维码
+ wifiInfo := "WIFI:T:WPA;S:MyWiFi;P:password123;;"
+ utils.GenerateQRCode(wifiInfo, "wifi_qr.png", 300)
+}
+```
-4. **数据库连接**
- ```go
- // 使用连接池优化
- db, err := database.NewDatabase("mysql", dsn, options)
- ```
+## 📝 使用建议
+
+### 1. 配置管理
+```go
+// 推荐使用环境变量进行配置
+conf.New("your-service", &config)
+```
+
+### 2. 错误处理
+```go
+// 使用统一的错误码
+if err != nil {
+ return errcode.ErrInternal
+}
+
+// 网络请求错误处理
+body, err := utils.HttpGet(url, 5*time.Second)
+if err != nil {
+ // 判断是否为超时错误
+ if errors.Is(err, context.DeadlineExceeded) {
+ fmt.Println("请求超时")
+ }
+ return err
+}
+```
+
+### 3. 缓存使用
+```go
+// 使用统一的缓存键前缀
+key := redisClient.BuildKey("user", userID)
+```
+
+### 4. 数据库连接
+```go
+// 使用连接池优化
+db, err := database.NewDatabase("mysql", dsn, options)
+```
+
+### 5. 二维码生成最佳实践
+```go
+// 内容较长时增大尺寸
+if len(content) > 100 {
+ size = 512
+} else {
+ size = 256
+}
+
+// 添加Logo时使用高级纠错
+config := &utils.QRCodeConfig{
+ Size: 512,
+ ErrorLevel: utils.QRCodeErrorLevelHigh, // 重要!
+ ForegroundColor: color.Black,
+ BackgroundColor: color.White,
+}
+```
## 许可证
diff --git a/go.mod b/go.mod
index f750145..ea02905 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,92 @@
module git.apinb.com/bsm-sdk/core
go 1.25.1
+
+require (
+ github.com/allegro/bigcache/v3 v3.1.0
+ github.com/elastic/go-elasticsearch/v9 v9.2.0
+ github.com/gin-contrib/cors v1.7.6
+ github.com/gin-gonic/gin v1.11.0
+ github.com/golang-jwt/jwt/v5 v5.3.0
+ github.com/google/uuid v1.6.0
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3
+ github.com/nats-io/nats.go v1.47.0
+ github.com/oklog/ulid/v2 v2.1.1
+ github.com/redis/go-redis/v9 v9.16.0
+ github.com/shirou/gopsutil v3.21.11+incompatible
+ go.etcd.io/etcd/client/pkg/v3 v3.6.5
+ go.etcd.io/etcd/client/v3 v3.6.5
+ google.golang.org/grpc v1.76.0
+ gopkg.in/yaml.v3 v3.0.1
+ gorm.io/driver/mysql v1.6.0
+ gorm.io/driver/postgres v1.6.0
+ gorm.io/gorm v1.31.1
+)
+
+require (
+ filippo.io/edwards25519 v1.1.0 // indirect
+ github.com/bytedance/sonic v1.14.0 // indirect
+ github.com/bytedance/sonic/loader v0.3.0 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/cloudwego/base64x v0.1.6 // indirect
+ github.com/coreos/go-semver v0.3.1 // indirect
+ github.com/coreos/go-systemd/v22 v22.5.0 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/elastic/elastic-transport-go/v8 v8.7.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.9 // indirect
+ github.com/gin-contrib/sse v1.1.0 // indirect
+ github.com/go-logr/logr v1.4.3 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-ole/go-ole v1.2.6 // 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.27.0 // indirect
+ github.com/go-sql-driver/mysql v1.8.1 // indirect
+ github.com/goccy/go-json v0.10.5 // indirect
+ github.com/goccy/go-yaml v1.18.0 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+ github.com/jackc/pgx/v5 v5.6.0 // indirect
+ github.com/jackc/puddle/v2 v2.2.2 // indirect
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/compress v1.18.0 // indirect
+ github.com/klauspost/cpuid/v2 v2.3.0 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/nats-io/nkeys v0.4.11 // indirect
+ github.com/nats-io/nuid v1.0.1 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/quic-go/qpack v0.5.1 // indirect
+ github.com/quic-go/quic-go v0.54.0 // indirect
+ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
+ github.com/tklauser/go-sysconf v0.3.15 // indirect
+ github.com/tklauser/numcpus v0.10.0 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.3.0 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
+ go.etcd.io/etcd/api/v3 v3.6.5 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/otel v1.37.0 // indirect
+ go.opentelemetry.io/otel/metric v1.37.0 // indirect
+ go.opentelemetry.io/otel/trace v1.37.0 // indirect
+ go.uber.org/mock v0.5.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ go.uber.org/zap v1.27.0 // indirect
+ golang.org/x/arch v0.20.0 // indirect
+ golang.org/x/crypto v0.41.0 // indirect
+ golang.org/x/mod v0.27.0 // indirect
+ golang.org/x/net v0.43.0 // indirect
+ golang.org/x/sync v0.17.0 // indirect
+ golang.org/x/sys v0.35.0 // indirect
+ golang.org/x/text v0.29.0 // indirect
+ golang.org/x/tools v0.36.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
+ google.golang.org/protobuf v1.36.10 // indirect
+)
diff --git a/go.sum b/go.sum
index e69de29..a5dbb43 100644
--- a/go.sum
+++ b/go.sum
@@ -0,0 +1,243 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk=
+github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
+github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
+github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
+github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
+github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
+github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
+github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
+github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
+github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
+github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
+github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
+github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+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/elastic/elastic-transport-go/v8 v8.7.0 h1:OgTneVuXP2uip4BA658Xi6Hfw+PeIOod2rY3GVMGoVE=
+github.com/elastic/elastic-transport-go/v8 v8.7.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk=
+github.com/elastic/go-elasticsearch/v9 v9.2.0 h1:COeL/g20+ixnUbffe4Wfbu88emrHjAq/LhVfmrjqRQs=
+github.com/elastic/go-elasticsearch/v9 v9.2.0/go.mod h1:2PB5YQPpY5tWbF65MRqzEXA31PZOdXCkloQSOZtU14I=
+github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
+github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
+github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
+github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
+github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
+github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
+github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
+github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
+github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
+github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+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/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
+github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
+github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
+github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+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=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+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.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
+github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
+github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
+github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
+github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
+github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
+github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
+github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
+github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
+github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
+github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
+github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
+github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
+github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
+github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
+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/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
+github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
+github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
+github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
+github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
+github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+go.etcd.io/etcd/api/v3 v3.6.5 h1:pMMc42276sgR1j1raO/Qv3QI9Af/AuyQUW6CBAWuntA=
+go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ=
+go.etcd.io/etcd/client/pkg/v3 v3.6.5 h1:Duz9fAzIZFhYWgRjp/FgNq2gO1jId9Yae/rLn3RrBP8=
+go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk=
+go.etcd.io/etcd/client/v3 v3.6.5 h1:yRwZNFBx/35VKHTcLDeO7XVLbCBFbPi+XV4OC3QJf2U=
+go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
+go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
+golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
+golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
+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.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
+golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
+golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
+golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+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.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
+golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
+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=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
+google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
+google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
+google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
+google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
+google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
+google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
+gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
+gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
+gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
+gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
+gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
diff --git a/utils/net.go b/utils/net.go
index 0b9c7eb..5e48db6 100644
--- a/utils/net.go
+++ b/utils/net.go
@@ -1,11 +1,56 @@
// Package utils 提供通用工具函数
// 包括数据类型转换、时间处理、网络工具等
+//
+// 网络工具模块
+//
+// 本模块提供了完整的网络相关工具函数,包括:
+//
+// IP地址相关:
+// - IsPublicIP: 判断IP是否为公网IP(识别私有网段)
+// - GetLocationIP: 获取本机第一个有效IPv4地址
+// - LocalIPv4s: 获取本机所有IPv4地址列表
+// - GetOutBoundIP: 获取外网IP地址
+//
+// HTTP请求相关(带超时保护):
+// - HttpGet: 发送HTTP GET请求
+// - HttpPost: 发送HTTP POST请求
+// - HttpPostJSON: 发送HTTP POST JSON请求
+// - HttpRequest: 执行自定义HTTP请求
+// - DownloadFile: 下载文件(支持进度回调)
+//
+// 性能特点:
+// - 所有HTTP请求都有超时保护(默认30秒)
+// - 支持自定义超时时间
+// - 使用Context进行超时控制
+// - 完善的错误处理
+// - 并发安全
+//
+// 使用示例:
+//
+// // 获取本机IP
+// localIP := utils.GetLocationIP()
+//
+// // HTTP GET请求(默认30秒超时)
+// body, _ := utils.HttpGet("https://api.example.com/data")
+//
+// // 自定义超时
+// body, _ := utils.HttpGet("https://api.example.com/data", 5*time.Second)
+//
+// // POST JSON数据
+// data := map[string]any{"key": "value"}
+// body, _ := utils.HttpPostJSON(url, headers, data)
+//
+// // 下载文件(带进度)
+// progress := func(total, downloaded int64) {
+// fmt.Printf("进度: %.2f%%\n", float64(downloaded)/float64(total)*100)
+// }
+// err := utils.DownloadFile(url, "file.zip", progress)
package utils
import (
"bytes"
+ "context"
"encoding/json"
- "errors"
"fmt"
"io"
"net"
@@ -13,6 +58,16 @@ import (
"os"
"strconv"
"strings"
+ "time"
+)
+
+const (
+ // DefaultHTTPTimeout HTTP请求默认超时时间
+ DefaultHTTPTimeout = 30 * time.Second
+ // DefaultDownloadTimeout 文件下载默认超时时间
+ DefaultDownloadTimeout = 5 * time.Minute
+ // DefaultBufferSize 默认缓冲区大小
+ DefaultBufferSize = 32 * 1024
)
// IsPublicIP 判断是否为公网IP
@@ -20,11 +75,17 @@ import (
// 返回: 是否为公网IP
func IsPublicIP(ipString string) bool {
ip := net.ParseIP(ipString)
+ if ip == nil {
+ return false
+ }
+
if ip.IsLoopback() || ip.IsLinkLocalMulticast() || ip.IsLinkLocalUnicast() {
return false
}
+
if ip4 := ip.To4(); ip4 != nil {
- switch true {
+ // 检查私有IP地址段
+ switch {
case ip4[0] == 10: // 10.0.0.0/8
return false
case ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31: // 172.16.0.0/12
@@ -39,70 +100,76 @@ func IsPublicIP(ipString string) bool {
}
// GetLocationIP 获取本机IP地址
-// 返回: 本机IP地址
-func GetLocationIP() (localIp string) {
- localIp = "127.0.0.1"
- // Get all network interfaces
+// 返回: 本机IP地址,如果找不到则返回 "127.0.0.1"
+func GetLocationIP() string {
+ localIP := "127.0.0.1"
+
+ // 获取所有网络接口
interfaces, err := net.Interfaces()
if err != nil {
- return
+ return localIP
}
for _, iface := range interfaces {
- // Skip the loopback interface
+ // 跳过回环接口
if iface.Flags&net.FlagLoopback != 0 {
continue
}
- // Get addresses associated with the interface
+ // 获取接口关联的地址
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
- // Check if the address is an IPNet
+ // 检查地址是否为 IPNet 类型
ipnet, ok := addr.(*net.IPNet)
if !ok || ipnet.IP.IsLoopback() {
continue
}
- // Get the IP address
+ // 获取 IPv4 地址
ip := ipnet.IP.To4()
if ip == nil {
continue
}
- // Skip IP addresses in the 169.254.x.x range
- if strings.HasPrefix(ip.String(), "169.254") {
+ ipStr := ip.String()
+ // 跳过链路本地地址 169.254.x.x 和虚拟网络地址 26.26.x.x
+ if strings.HasPrefix(ipStr, "169.254") || strings.HasPrefix(ipStr, "26.26") {
continue
}
- // Skip IP addresses in the 169.254.x.x range
- if strings.HasPrefix(ip.String(), "26.26") {
- continue
- }
-
- // Return the first valid IP address found
- return ip.String()
+ // 返回找到的第一个有效 IP 地址
+ return ipStr
}
}
- return
+ return localIP
}
+// LocalIPv4s 获取本机所有IPv4地址
+// 返回: IPv4地址列表和错误信息
func LocalIPv4s() ([]string, error) {
-
var ips []string
- addrs, _ := net.InterfaceAddrs()
+
+ addrs, err := net.InterfaceAddrs()
+ if err != nil {
+ return nil, err
+ }
for _, addr := range addrs {
- if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
- if ipnet.IP.To4() != nil {
- locIP := ipnet.IP.To4().String()
- if locIP[0:7] != "169.254" {
- ips = append(ips, locIP)
- }
+ ipnet, ok := addr.(*net.IPNet)
+ if !ok || ipnet.IP.IsLoopback() {
+ continue
+ }
+
+ if ipv4 := ipnet.IP.To4(); ipv4 != nil {
+ ipStr := ipv4.String()
+ // 跳过链路本地地址
+ if !strings.HasPrefix(ipStr, "169.254") {
+ ips = append(ips, ipStr)
}
}
}
@@ -110,50 +177,97 @@ func LocalIPv4s() ([]string, error) {
return ips, nil
}
-func GetOutBoundIP() (ip string, err error) {
- body, err := HttpGet("http://ip.dhcp.cn/?ip") // 获取外网 IP
- return string(body), err
+// GetOutBoundIP 获取外网IP地址
+// 返回: 外网IP地址字符串和错误信息
+func GetOutBoundIP() (string, error) {
+ body, err := HttpGet("http://ip.dhcp.cn/?ip")
+ if err != nil {
+ return "", err
+ }
+ return string(body), nil
}
-func HttpGet(url string) ([]byte, error) {
- resp, err := http.Get(url)
+// getTimeoutDuration 获取超时时间,如果未指定则使用默认值
+func getTimeoutDuration(timeout []time.Duration, defaultTimeout time.Duration) time.Duration {
+ if len(timeout) > 0 {
+ return timeout[0]
+ }
+ return defaultTimeout
+}
+
+// createHTTPClient 创建带超时的HTTP客户端
+func createHTTPClient(timeout time.Duration) *http.Client {
+ return &http.Client{
+ Timeout: timeout,
+ }
+}
+
+// HttpGet 发送HTTP GET请求
+// url: 请求地址
+// timeout: 超时时间(可选,默认30秒),可以传入多个,只使用第一个
+// 返回: 响应体和错误信息
+func HttpGet(url string, timeout ...time.Duration) ([]byte, error) {
+ timeoutDuration := getTimeoutDuration(timeout, DefaultHTTPTimeout)
+
+ ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration)
+ defer cancel()
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
- // handle error
return nil, err
}
+ client := createHTTPClient(timeoutDuration)
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
defer resp.Body.Close()
- body, err := io.ReadAll(resp.Body)
- return body, err
+ return io.ReadAll(resp.Body)
}
+// HttpPostJSON 发送HTTP POST JSON请求
+// url: 请求地址
+// header: 请求头
+// data: 请求数据(将被序列化为JSON)
+// 返回: 响应体和错误信息
func HttpPostJSON(url string, header map[string]string, data map[string]any) ([]byte, error) {
- bytes, err := json.Marshal(data)
+ jsonBytes, err := json.Marshal(data)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("marshal json failed: %w", err)
}
- return HttpPost(url, header, bytes)
+ return HttpPost(url, header, jsonBytes)
}
-func HttpPost(url string, header map[string]string, data []byte) ([]byte, error) {
- var err error
- reader := bytes.NewBuffer(data)
- request, err := http.NewRequest("POST", url, reader)
+// HttpPost 发送HTTP POST请求
+// url: 请求地址
+// header: 请求头
+// data: 请求体数据
+// timeout: 超时时间(可选,默认30秒),可以传入多个,只使用第一个
+// 返回: 响应体和错误信息
+func HttpPost(url string, header map[string]string, data []byte, timeout ...time.Duration) ([]byte, error) {
+ timeoutDuration := getTimeoutDuration(timeout, DefaultHTTPTimeout)
+ ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration)
+ defer cancel()
+
+ request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
+ // 设置默认请求头
request.Header.Set("Content-Type", "application/json;charset=UTF-8")
request.Header.Set("Request-Id", ULID())
+ // 设置自定义请求头
for key, val := range header {
request.Header.Set(key, val)
}
- client := http.Client{}
+ client := createHTTPClient(timeoutDuration)
resp, err := client.Do(request)
if err != nil {
return nil, err
@@ -165,97 +279,112 @@ func HttpPost(url string, header map[string]string, data []byte) ([]byte, error)
return nil, err
}
- if resp.StatusCode != 200 {
- return nil, errors.New(string(respBytes))
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("http status %d: %s", resp.StatusCode, string(respBytes))
}
return respBytes, nil
}
-func HttpRequest(r *http.Request) ([]byte, error) {
- var err error
- client := http.Client{}
+// HttpRequest 执行HTTP请求
+// r: HTTP请求对象
+// timeout: 超时时间(可选,默认30秒),可以传入多个,只使用第一个
+// 返回: 响应体和错误信息
+func HttpRequest(r *http.Request, timeout ...time.Duration) ([]byte, error) {
+ timeoutDuration := getTimeoutDuration(timeout, DefaultHTTPTimeout)
+
+ // 如果请求还没有设置context,添加一个带超时的context
+ if r.Context() == context.Background() || r.Context() == nil {
+ ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration)
+ defer cancel()
+ r = r.WithContext(ctx)
+ }
+
+ client := createHTTPClient(timeoutDuration)
resp, err := client.Do(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
- respBytes, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, err
- }
-
- return respBytes, nil
+ return io.ReadAll(resp.Body)
}
-func DownloadFile(url, saveTo string, fb func(length, downLen int64)) {
- var (
- fsize int64
- buf = make([]byte, 32*1024)
- written int64
- )
- //创建一个http client
- client := new(http.Client)
- //get方法获取资源
- resp, err := client.Get(url)
+// DownloadFile 下载文件
+// url: 下载地址
+// saveTo: 保存路径
+// fb: 进度回调函数
+// timeout: 超时时间(可选,默认5分钟),可以传入多个,只使用第一个
+func DownloadFile(url, saveTo string, fb func(length, downLen int64), timeout ...time.Duration) error {
+ timeoutDuration := getTimeoutDuration(timeout, DefaultDownloadTimeout)
+
+ ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration)
+ defer cancel()
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
- fmt.Printf("download %s error:%s\n", url, err)
- return
+ return fmt.Errorf("create request error: %w", err)
}
- //读取服务器返回的文件大小
- fsize, err = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 32)
+
+ client := createHTTPClient(timeoutDuration)
+ resp, err := client.Do(req)
if err != nil {
- fmt.Println(err)
- }
- //创建文件
- file, err := os.Create(saveTo)
- file.Chmod(0777)
- if err != nil {
- fmt.Printf("Create %s error:%s\n", saveTo, err)
- return
- }
- defer file.Close()
- if resp.Body == nil {
- fmt.Printf("resp %s error:%s\n", url, err)
- return
+ return fmt.Errorf("download %s error: %w", url, err)
}
defer resp.Body.Close()
- //下面是 io.copyBuffer() 的简化版本
+
+ if resp.Body == nil {
+ return fmt.Errorf("response body is nil for %s", url)
+ }
+
+ // 读取服务器返回的文件大小
+ fsize, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
+ if err != nil {
+ // 如果无法获取文件大小,设置为-1表示未知
+ fsize = -1
+ }
+
+ // 创建文件
+ file, err := os.Create(saveTo)
+ if err != nil {
+ return fmt.Errorf("create file %s error: %w", saveTo, err)
+ }
+ defer file.Close()
+
+ // 设置文件权限
+ if err := file.Chmod(0644); err != nil {
+ return fmt.Errorf("chmod file %s error: %w", saveTo, err)
+ }
+
+ // 使用缓冲区读取并写入文件,同时调用进度回调
+ buf := make([]byte, DefaultBufferSize)
+ var written int64
+
for {
- //读取bytes
- nr, er := resp.Body.Read(buf)
+ nr, readErr := resp.Body.Read(buf)
if nr > 0 {
- //写入bytes
- nw, ew := file.Write(buf[0:nr])
- //数据长度大于0
+ nw, writeErr := file.Write(buf[0:nr])
if nw > 0 {
written += int64(nw)
+ // 调用进度回调
+ if fb != nil {
+ fb(fsize, written)
+ }
}
- //写入出错
- if ew != nil {
- err = ew
- break
+ if writeErr != nil {
+ return fmt.Errorf("write file error: %w", writeErr)
}
- //读取是数据长度不等于写入的数据长度
if nr != nw {
- err = io.ErrShortWrite
+ return fmt.Errorf("write file error: %w", io.ErrShortWrite)
+ }
+ }
+ if readErr != nil {
+ if readErr == io.EOF {
break
}
+ return fmt.Errorf("read response error: %w", readErr)
}
- if er != nil {
- if er != io.EOF {
- err = er
- }
- break
- }
- //没有错误了快使用 callback
-
- fb(fsize, written)
}
- if err != nil {
- fmt.Printf("callback error:%s\n", err)
- return
- }
+ return nil
}
diff --git a/utils/qrcode.go b/utils/qrcode.go
new file mode 100644
index 0000000..faba89d
--- /dev/null
+++ b/utils/qrcode.go
@@ -0,0 +1,457 @@
+// Package utils 提供通用工具函数
+//
+// 二维码生成功能模块
+//
+// 本模块提供了完整的二维码生成功能,支持:
+// - 基础二维码生成(保存为PNG文件)
+// - 生成字节数组(可用于HTTP响应、数据库存储等)
+// - Base64编码输出(便于存储和传输)
+// - Data URL格式(可直接用于HTML
标签)
+// - 自定义配置(尺寸、颜色、纠错级别)
+// - 带Logo的二维码
+// - 批量生成二维码
+package utils
+
+import (
+ "bytes"
+ "encoding/base64"
+ "fmt"
+ "image"
+ "image/color"
+ "image/png"
+ "os"
+
+ "github.com/skip2/go-qrcode"
+)
+
+// QRCodeErrorLevel 二维码纠错级别
+//
+// 纠错级别决定了二维码能容忍多少损坏:
+// - 级别越高,容错能力越强,但二维码会更复杂
+// - 添加Logo时建议使用高级纠错
+// - 一般场景使用中级纠错即可
+type QRCodeErrorLevel int
+
+const (
+ // QRCodeErrorLevelLow 低级纠错(约7%容错)
+ // 适用场景:环境良好、追求小尺寸、内容较少
+ QRCodeErrorLevelLow QRCodeErrorLevel = iota
+
+ // QRCodeErrorLevelMedium 中级纠错(约15%容错)
+ // 适用场景:通用场景(默认推荐)
+ QRCodeErrorLevelMedium
+
+ // QRCodeErrorLevelQuartile 较高级纠错(约25%容错)
+ // 适用场景:需要较高容错能力
+ QRCodeErrorLevelQuartile
+
+ // QRCodeErrorLevelHigh 高级纠错(约30%容错)
+ // 适用场景:添加Logo、容易损坏的环境、长期使用
+ QRCodeErrorLevelHigh
+)
+
+const (
+ // DefaultQRCodeSize 默认二维码尺寸(像素)
+ DefaultQRCodeSize = 256
+ // MinQRCodeSize 最小二维码尺寸
+ MinQRCodeSize = 21
+ // MaxQRCodeSize 最大二维码尺寸
+ MaxQRCodeSize = 8192
+)
+
+// QRCodeConfig 二维码配置结构
+//
+// 用于自定义二维码的外观和质量参数
+//
+// 示例:
+//
+// config := &QRCodeConfig{
+// Size: 512, // 尺寸512x512像素
+// ErrorLevel: QRCodeErrorLevelHigh, // 高级纠错
+// ForegroundColor: color.RGBA{255, 0, 0, 255}, // 红色二维码
+// BackgroundColor: color.White, // 白色背景
+// }
+type QRCodeConfig struct {
+ Size int // 尺寸(像素),范围:21-8192
+ ErrorLevel QRCodeErrorLevel // 纠错级别,影响容错能力和复杂度
+ ForegroundColor color.Color // 前景色(二维码颜色),建议使用深色
+ BackgroundColor color.Color // 背景色,建议使用浅色以保证对比度
+}
+
+// DefaultQRCodeConfig 返回默认配置
+//
+// 默认配置适用于大多数场景:
+// - 尺寸:256x256像素(适合手机扫描)
+// - 纠错级别:中级(15%容错)
+// - 颜色:黑白配色(最佳识别率)
+//
+// 返回值:
+//
+// *QRCodeConfig: 默认配置对象
+func DefaultQRCodeConfig() *QRCodeConfig {
+ return &QRCodeConfig{
+ Size: DefaultQRCodeSize,
+ ErrorLevel: QRCodeErrorLevelMedium,
+ ForegroundColor: color.Black,
+ BackgroundColor: color.White,
+ }
+}
+
+// convertErrorLevel 转换纠错级别为底层库的纠错级别
+//
+// 将自定义的纠错级别枚举转换为 go-qrcode 库所需的格式
+//
+// 参数:
+//
+// level: 自定义纠错级别
+//
+// 返回值:
+//
+// qrcode.RecoveryLevel: go-qrcode库的纠错级别
+func convertErrorLevel(level QRCodeErrorLevel) qrcode.RecoveryLevel {
+ switch level {
+ case QRCodeErrorLevelLow:
+ return qrcode.Low
+ case QRCodeErrorLevelMedium:
+ return qrcode.Medium
+ case QRCodeErrorLevelQuartile:
+ return qrcode.High
+ case QRCodeErrorLevelHigh:
+ return qrcode.Highest
+ default:
+ return qrcode.Medium
+ }
+}
+
+// GenerateQRCode 生成二维码并保存为PNG文件
+//
+// 这是最简单的二维码生成方法,适合快速生成标准黑白二维码。
+// 使用中级纠错,黑白配色,PNG格式输出。
+//
+// 参数:
+//
+// content: 二维码内容(支持URL、文本、vCard、WiFi等格式)
+// filename: 保存的文件路径(.png文件)
+// size: 二维码尺寸(可选,单位:像素)
+// - 不传参数:使用默认256x256
+// - 传一个参数:使用指定尺寸
+// - 有效范围:21-8192像素
+//
+// 返回值:
+//
+// error: 错误信息,成功时返回nil
+//
+// 注意事项:
+// - 内容越长,二维码越复杂,建议尺寸>=256
+// - 文件权限为0644
+// - 会覆盖已存在的同名文件
+func GenerateQRCode(content, filename string, size ...int) error {
+ qrSize := DefaultQRCodeSize
+ if len(size) > 0 {
+ qrSize = size[0]
+ if qrSize < MinQRCodeSize || qrSize > MaxQRCodeSize {
+ return fmt.Errorf("二维码尺寸必须在 %d 到 %d 之间", MinQRCodeSize, MaxQRCodeSize)
+ }
+ }
+
+ err := qrcode.WriteFile(content, qrcode.Medium, qrSize, filename)
+ if err != nil {
+ return fmt.Errorf("生成二维码失败: %w", err)
+ }
+
+ return nil
+}
+
+// GenerateQRCodeBytes 生成二维码字节数组(PNG格式)
+//
+// 生成二维码的字节数组而不保存到文件,适合:
+// - HTTP响应直接返回图片
+// - 存储到数据库
+// - 通过网络传输
+// - 进一步处理(如添加到PDF)
+//
+// 参数:
+//
+// content: 二维码内容
+// size: 二维码尺寸(可选,默认256)
+//
+// 返回值:
+//
+// []byte: PNG格式的图片字节数组
+// error: 错误信息
+func GenerateQRCodeBytes(content string, size ...int) ([]byte, error) {
+ qrSize := DefaultQRCodeSize
+ if len(size) > 0 {
+ qrSize = size[0]
+ if qrSize < MinQRCodeSize || qrSize > MaxQRCodeSize {
+ return nil, fmt.Errorf("二维码尺寸必须在 %d 到 %d 之间", MinQRCodeSize, MaxQRCodeSize)
+ }
+ }
+
+ bytes, err := qrcode.Encode(content, qrcode.Medium, qrSize)
+ if err != nil {
+ return nil, fmt.Errorf("生成二维码失败: %w", err)
+ }
+
+ return bytes, nil
+}
+
+// GenerateQRCodeBase64 生成Base64编码的二维码字符串
+//
+// 将二维码图片编码为Base64字符串,便于:
+// - 存储到数据库的文本字段
+// - 通过JSON/XML传输
+// - 避免二进制数据处理问题
+//
+// 参数:
+//
+// content: 二维码内容
+// size: 二维码尺寸(可选,默认256)
+//
+// 返回值:
+//
+// string: Base64编码的字符串(不包含data:image/png;base64,前缀)
+// error: 错误信息
+func GenerateQRCodeBase64(content string, size ...int) (string, error) {
+ qrBytes, err := GenerateQRCodeBytes(content, size...)
+ if err != nil {
+ return "", err
+ }
+
+ base64Str := base64.StdEncoding.EncodeToString(qrBytes)
+ return base64Str, nil
+}
+
+// GenerateQRCodeWithConfig 使用自定义配置生成二维码
+//
+// 提供完全自定义的二维码生成能力,可以控制:
+// - 尺寸大小
+// - 纠错级别
+// - 前景色和背景色
+//
+// 参数:
+//
+// content: 二维码内容
+// config: 二维码配置对象(nil时使用默认配置)
+//
+// 返回值:
+//
+// []byte: PNG格式的字节数组
+// error: 错误信息
+//
+// 注意事项:
+// - 确保前景色和背景色有足够对比度
+// - 浅色前景配深色背景可能影响扫描
+func GenerateQRCodeWithConfig(content string, config *QRCodeConfig) ([]byte, error) {
+ if config == nil {
+ config = DefaultQRCodeConfig()
+ }
+
+ // 验证尺寸
+ if config.Size < MinQRCodeSize || config.Size > MaxQRCodeSize {
+ return nil, fmt.Errorf("二维码尺寸必须在 %d 到 %d 之间", MinQRCodeSize, MaxQRCodeSize)
+ }
+
+ // 创建二维码对象
+ qr, err := qrcode.New(content, convertErrorLevel(config.ErrorLevel))
+ if err != nil {
+ return nil, fmt.Errorf("创建二维码失败: %w", err)
+ }
+
+ // 设置颜色
+ qr.ForegroundColor = config.ForegroundColor
+ qr.BackgroundColor = config.BackgroundColor
+
+ // 生成PNG
+ pngBytes, err := qr.PNG(config.Size)
+ if err != nil {
+ return nil, fmt.Errorf("生成PNG失败: %w", err)
+ }
+
+ return pngBytes, nil
+}
+
+// GenerateQRCodeWithLogo 生成带Logo的二维码
+//
+// 在二维码中心嵌入Logo图片,Logo会占据二维码约1/5的区域。
+// 使用高级纠错以确保Logo不影响二维码的可读性。
+//
+// 参数:
+//
+// content: 二维码内容
+// logoPath: Logo图片文件路径(支持PNG、JPEG等格式)
+// size: 二维码尺寸(可选,默认256,建议>=512以保证清晰度)
+//
+// 返回值:
+//
+// []byte: PNG格式的字节数组
+// error: 错误信息
+//
+// 注意事项:
+// - Logo会自动缩放到二维码的1/5大小
+// - 建议Logo使用正方形图片
+// - 使用高级纠错级别(~30%容错)
+// - Logo会覆盖二维码中心区域
+// - 建议二维码尺寸>=512以保证Logo清晰
+// - Logo文件必须存在且可读取
+func GenerateQRCodeWithLogo(content, logoPath string, size ...int) ([]byte, error) {
+ qrSize := DefaultQRCodeSize
+ if len(size) > 0 {
+ qrSize = size[0]
+ if qrSize < MinQRCodeSize || qrSize > MaxQRCodeSize {
+ return nil, fmt.Errorf("二维码尺寸必须在 %d 到 %d 之间", MinQRCodeSize, MaxQRCodeSize)
+ }
+ }
+
+ // 生成基础二维码
+ qr, err := qrcode.New(content, qrcode.High)
+ if err != nil {
+ return nil, fmt.Errorf("创建二维码失败: %w", err)
+ }
+
+ // 生成二维码图像
+ qrImage := qr.Image(qrSize)
+
+ // 读取Logo图片
+ logoFile, err := os.Open(logoPath)
+ if err != nil {
+ return nil, fmt.Errorf("打开Logo文件失败: %w", err)
+ }
+ defer logoFile.Close()
+
+ logoImage, _, err := image.Decode(logoFile)
+ if err != nil {
+ return nil, fmt.Errorf("解码Logo图片失败: %w", err)
+ }
+
+ // 计算Logo位置和大小(Logo占二维码的1/5)
+ logoSize := qrSize / 5
+ logoX := (qrSize - logoSize) / 2
+ logoY := (qrSize - logoSize) / 2
+
+ // 创建最终图像
+ finalImage := image.NewRGBA(qrImage.Bounds())
+
+ // 绘制二维码
+ for y := 0; y < qrSize; y++ {
+ for x := 0; x < qrSize; x++ {
+ finalImage.Set(x, y, qrImage.At(x, y))
+ }
+ }
+
+ // 绘制Logo
+ logoOriginalBounds := logoImage.Bounds()
+ for y := 0; y < logoSize; y++ {
+ for x := 0; x < logoSize; x++ {
+ // 计算原始Logo的对应像素
+ origX := x * logoOriginalBounds.Dx() / logoSize
+ origY := y * logoOriginalBounds.Dy() / logoSize
+ finalImage.Set(logoX+x, logoY+y, logoImage.At(origX, origY))
+ }
+ }
+
+ // 转换为PNG字节
+ var buf bytes.Buffer
+ if err := png.Encode(&buf, finalImage); err != nil {
+ return nil, fmt.Errorf("编码PNG失败: %w", err)
+ }
+
+ return buf.Bytes(), nil
+}
+
+// SaveQRCodeBytes 保存二维码字节数组到文件
+//
+// 将二维码字节数组保存为PNG文件,常与 GenerateQRCodeBytes 或
+// GenerateQRCodeWithConfig 配合使用。
+//
+// 参数:
+//
+// data: PNG格式的二维码字节数组
+// filename: 保存的文件路径
+//
+// 返回值:
+//
+// error: 错误信息
+//
+// 注意事项:
+// - 文件权限为0644
+// - 会覆盖已存在的文件
+// - 确保目录已存在
+func SaveQRCodeBytes(data []byte, filename string) error {
+ if err := os.WriteFile(filename, data, 0644); err != nil {
+ return fmt.Errorf("保存文件失败: %w", err)
+ }
+ return nil
+}
+
+// GenerateQRCodeDataURL 生成Data URL格式的二维码
+//
+// 生成可以直接用于HTML
标签的Data URL格式字符串。
+// Data URL包含了完整的图片数据,无需额外的图片文件。
+//
+// 参数:
+//
+// content: 二维码内容
+// size: 二维码尺寸(可选,默认256)
+//
+// 返回值:
+//
+// string: Data URL格式字符串(包含data:image/png;base64,前缀)
+// error: 错误信息
+//
+// 注意事项:
+// - Data URL字符串较长,不适合存储到数据库
+// - 适合临时显示、前端渲染等场景
+// - 某些老旧浏览器可能不支持
+func GenerateQRCodeDataURL(content string, size ...int) (string, error) {
+ qrBytes, err := GenerateQRCodeBytes(content, size...)
+ if err != nil {
+ return "", err
+ }
+
+ base64Str := base64.StdEncoding.EncodeToString(qrBytes)
+ dataURL := fmt.Sprintf("data:image/png;base64,%s", base64Str)
+ return dataURL, nil
+}
+
+// BatchGenerateQRCode 批量生成二维码文件
+//
+// 一次性生成多个二维码文件,适合:
+// - 批量生成产品二维码
+// - 生成多个用户的会员卡
+// - 批量生成门票、优惠券等
+//
+// 参数:
+//
+// items: map[内容]文件名,例如:map["产品A":"qr_a.png", "产品B":"qr_b.png"]
+// size: 二维码尺寸(可选,默认256,所有二维码使用相同尺寸)
+//
+// 返回值:
+//
+// []string: 失败的文件名列表(成功时为nil)
+// error: 错误信息(部分失败时返回错误,但成功的文件已生成)
+//
+// 注意事项:
+// - 即使部分失败,成功的二维码仍会生成
+// - 建议检查返回的failed列表以确定哪些失败了
+// - 大批量生成时注意磁盘空间
+func BatchGenerateQRCode(items map[string]string, size ...int) ([]string, error) {
+ var failed []string
+ qrSize := DefaultQRCodeSize
+ if len(size) > 0 {
+ qrSize = size[0]
+ }
+
+ for content, filename := range items {
+ err := GenerateQRCode(content, filename, qrSize)
+ if err != nil {
+ failed = append(failed, filename)
+ }
+ }
+
+ if len(failed) > 0 {
+ return failed, fmt.Errorf("有 %d 个二维码生成失败", len(failed))
+ }
+
+ return nil, nil
+}