Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 240d0f1e7c | |||
|
|
2c5aab84da | ||
| 3271453878 | |||
| 42e9d55b62 | |||
| 179157f49e | |||
| 8aafcbd91c | |||
| 7e91109bce | |||
| d451eb3eff | |||
| 3d71936ecf | |||
| 524e310dfe | |||
| 409cb53e8c | |||
| 404957f16a | |||
| 3d6871138a | |||
| f7d8988415 | |||
| 820d7a5c63 | |||
| 3038c6c22c |
335
README.md
335
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 <img>标签
|
||||
- 包含完整图片数据
|
||||
- 示例:`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,
|
||||
}
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
10
conf/new.go
10
conf/new.go
@@ -111,11 +111,11 @@ func InitLoggerConf(cfg *LogConf) *LogConf {
|
||||
return &LogConf{
|
||||
Name: strings.ToLower(vars.ServiceKey),
|
||||
Level: vars.LogLevel(vars.DEBUG),
|
||||
Dir: cfg.Dir,
|
||||
Endpoint: cfg.Endpoint,
|
||||
Console: cfg.Console,
|
||||
File: cfg.File,
|
||||
Remote: cfg.Remote,
|
||||
Dir: "./logs/",
|
||||
Endpoint: "",
|
||||
Console: true,
|
||||
File: true,
|
||||
Remote: false,
|
||||
}
|
||||
}
|
||||
return cfg
|
||||
|
||||
@@ -28,7 +28,7 @@ func New(secret string) {
|
||||
|
||||
func GenerateTokenAes(id uint, identity, client, role string, owner any, extend map[string]string) (string, error) {
|
||||
if !(JwtSecretLen == 16 || JwtSecretLen == 24 || JwtSecretLen == 32) {
|
||||
return "", errcode.ErrJWTSecretKey
|
||||
return "", errcode.ErrTokenSecretKey
|
||||
}
|
||||
expireTime := time.Now().Add(vars.JwtExpire)
|
||||
claims := types.JwtClaims{
|
||||
@@ -43,7 +43,7 @@ func GenerateTokenAes(id uint, identity, client, role string, owner any, extend
|
||||
|
||||
byte, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
return "", errcode.ErrJWTJsonEncode
|
||||
return "", errcode.ErrTokenJsonEncode
|
||||
}
|
||||
|
||||
token, err := AesEncryptCBC(byte)
|
||||
@@ -59,7 +59,7 @@ func AesEncryptCBC(plan []byte) (string, error) {
|
||||
// NewCipher该函数限制了输入k的长度必须为16, 24或者32
|
||||
block, err := aes.NewCipher(JwtSecret)
|
||||
if err != nil {
|
||||
return "", errcode.ErrJWTSecretKey
|
||||
return "", errcode.ErrTokenSecretKey
|
||||
}
|
||||
// 获取秘钥块的长度
|
||||
blockSize := block.BlockSize()
|
||||
@@ -76,17 +76,17 @@ func AesEncryptCBC(plan []byte) (string, error) {
|
||||
|
||||
func AesDecryptCBC(cryted string) (b []byte, err error) {
|
||||
if (JwtSecretLen == 16 || JwtSecretLen == 24 || JwtSecretLen == 32) == false {
|
||||
return nil, errcode.ErrJWTSecretKey
|
||||
return nil, errcode.ErrTokenSecretKey
|
||||
}
|
||||
// 转成字节数组
|
||||
crytedByte, err := base64.StdEncoding.DecodeString(cryted)
|
||||
if err != nil {
|
||||
return nil, errcode.ErrJWTBase64Decode
|
||||
return nil, errcode.ErrTokenBase64Decode
|
||||
}
|
||||
// 分组秘钥
|
||||
block, err := aes.NewCipher(JwtSecret)
|
||||
if err != nil {
|
||||
return nil, errcode.ErrJWTSecretKey
|
||||
return nil, errcode.ErrTokenSecretKey
|
||||
}
|
||||
// 获取秘钥块的长度
|
||||
blockSize := block.BlockSize()
|
||||
@@ -99,7 +99,7 @@ func AesDecryptCBC(cryted string) (b []byte, err error) {
|
||||
// 去补全码
|
||||
orig = PKCS7UnPadding(orig, blockSize)
|
||||
if orig == nil {
|
||||
return nil, errcode.ErrJWTAuthParseFail
|
||||
return nil, errcode.ErrTokenAuthParseFail
|
||||
}
|
||||
return orig, nil
|
||||
}
|
||||
@@ -152,12 +152,12 @@ func ParseTokenAes(token string) (*types.JwtClaims, error) {
|
||||
var ac *types.JwtClaims
|
||||
err = json.Unmarshal(data, &ac)
|
||||
if err != nil {
|
||||
return nil, errcode.ErrJWTAuthParseFail
|
||||
return nil, errcode.ErrTokenAuthParseFail
|
||||
}
|
||||
|
||||
expireTime := time.Now().Unix()
|
||||
if expireTime > ac.ExpiresAt {
|
||||
return nil, errcode.ErrJWTAuthExpire
|
||||
return nil, errcode.ErrTokenAuthExpire
|
||||
}
|
||||
|
||||
return ac, nil
|
||||
|
||||
98
crypto/token/jwt.go
Normal file
98
crypto/token/jwt.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/vars"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
ID uint `json:"id"`
|
||||
Identity string `json:"identity"`
|
||||
Extend map[string]string `json:"extend"`
|
||||
Client string `json:"client"`
|
||||
Owner any `json:"owner"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims // v5版本新加的方法
|
||||
}
|
||||
|
||||
type tokenJwt struct {
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
func New(secretKey string) *tokenJwt {
|
||||
return &tokenJwt{SecretKey: secretKey}
|
||||
}
|
||||
|
||||
// 生成JWT
|
||||
func (t *tokenJwt) GenerateJwt(id uint, identity, client, role string, owner any, extend map[string]string) (string, error) {
|
||||
keyLen := len(t.SecretKey)
|
||||
if !(keyLen == 16 || keyLen == 24 || keyLen == 32) {
|
||||
return "", errcode.ErrTokenSecretKey
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
claims := Claims{
|
||||
ID: id,
|
||||
Identity: identity,
|
||||
Client: client,
|
||||
Extend: extend,
|
||||
Owner: owner,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(vars.JwtExpire)), // 过期时间24小时
|
||||
IssuedAt: jwt.NewNumericDate(now), // 签发时间
|
||||
NotBefore: jwt.NewNumericDate(now), // 生效时间
|
||||
},
|
||||
}
|
||||
// 使用HS256签名算法
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
s, err := token.SignedString([]byte(t.SecretKey))
|
||||
if err != nil {
|
||||
return "", errcode.String(errcode.ErrTokenGenerate, err.Error())
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// 解析JWT
|
||||
func (t *tokenJwt) ParseJwt(tokenstring string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenstring, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(t.SecretKey), nil
|
||||
})
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
} else {
|
||||
return nil, errcode.String(errcode.ErrTokenParse, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 验证JWT是否过期
|
||||
func (t *tokenJwt) IsExpired(tokenstring string) (bool, error) {
|
||||
// 分割JWT的三个部分
|
||||
parts := strings.Split(tokenstring, ".")
|
||||
if len(parts) != 3 {
|
||||
return true, errcode.ErrTokenDataInvalid
|
||||
}
|
||||
|
||||
// 解码Payload部分
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return true, errcode.String(errcode.ErrTokenBase64Decode, err.Error())
|
||||
}
|
||||
|
||||
// 解析JSON
|
||||
var claims jwt.RegisteredClaims
|
||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||
return true, errcode.String(errcode.ErrTokenJsonDecode, err.Error())
|
||||
}
|
||||
|
||||
// 检查过期时间
|
||||
currentTime := time.Now().Unix()
|
||||
return claims.ExpiresAt.Unix() < currentTime, nil
|
||||
}
|
||||
@@ -133,3 +133,13 @@ func NewPostgres(dsn []string, options *types.SqlOptions) (gormDb *gorm.DB, err
|
||||
|
||||
return gormDb, nil
|
||||
}
|
||||
|
||||
// AppendMigrate 调用此函数后,会在数据库初始化时自动迁移表结构
|
||||
//
|
||||
// - table: 需要自动迁移的表
|
||||
func AppendMigrate(table any) {
|
||||
if MigrateTables == nil {
|
||||
MigrateTables = make([]any, 0)
|
||||
}
|
||||
MigrateTables = append(MigrateTables, table)
|
||||
}
|
||||
|
||||
2
env/env.go
vendored
2
env/env.go
vendored
@@ -15,7 +15,7 @@ var Runtime *types.RuntimeEnv = nil
|
||||
func NewEnv() *types.RuntimeEnv {
|
||||
if Runtime == nil {
|
||||
Runtime = &types.RuntimeEnv{
|
||||
Workspace: GetEnvDefault("BSM_Workspace", "def"),
|
||||
Workspace: GetEnvDefault("BSM_Workspace", "default"),
|
||||
JwtSecretKey: GetEnvDefault("BSM_JwtSecretKey", "Cblocksmesh2022C"),
|
||||
Mode: strings.ToLower(GetEnvDefault("BSM_RuntimeMode", "dev")),
|
||||
LicencePath: strings.ToLower(GetEnvDefault("BSM_Licence", "")),
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
package errcode
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// HTTP请求头相关错误码,起始码:1000
|
||||
var (
|
||||
AllErrors = make(map[int]string)
|
||||
ErrHeaderRequestId = NewError(1001, "Header Request-Id Not Found") // 请求ID头缺失
|
||||
ErrHeaderAuthorization = NewError(1002, "Header Authorization Not Found") // 授权头缺失
|
||||
ErrHeaderSecretKey = NewError(1003, "Header Secret-Key Not Found") // 密钥头缺失
|
||||
@@ -32,18 +31,22 @@ var (
|
||||
ErrRecordNotFound = NewError(1112, "Record Not Found") // 记录未找到
|
||||
)
|
||||
|
||||
// JWT认证相关错误码,起始码:1300
|
||||
// Token认证相关错误码,起始码:1300
|
||||
var (
|
||||
ErrJWTAuthNotFound = NewError(1301, "JWT Authorization Not Found") // JWT授权未找到
|
||||
ErrJWTBase64Decode = NewError(1302, "JWT Authorization Base64 Decode Error") // JWT Base64解码错误
|
||||
ErrJWTAuthParseFail = NewError(1303, "JWT Authorization Fail") // JWT授权解析失败
|
||||
ErrJWTAuthKeyId = NewError(1304, "JWT Key:Id Incorrect") // JWT密钥ID错误
|
||||
ErrJWTAuthKeyIdentity = NewError(1305, "JWT Key:Identity Incorrect") // JWT密钥身份错误
|
||||
ErrJWTAuthTokenChanged = NewError(1306, "JWT Authorization Changed") // JWT授权已变更
|
||||
ErrJWTAuthExpire = NewError(1307, "JWT Authorization Expire") // JWT授权已过期
|
||||
ErrJWTJsonDecode = NewError(1308, "JWT Authorization JSON Decode Error") // JWT JSON解码错误
|
||||
ErrJWTJsonEncode = NewError(1309, "JWT Authorization JSON Encode Error") // JWT JSON编码错误
|
||||
ErrJWTSecretKey = NewError(1310, "JWT SecretKey Error") // JWT密钥错误
|
||||
ErrTokenAuthNotFound = NewError(1301, "Token Authorization Not Found") // Token授权未找到
|
||||
ErrTokenDataInvalid = NewError(1302, "Token Authorization Data Invalid") // Token授权数据无效
|
||||
ErrTokenBase64Decode = NewError(1303, "Token Authorization Base64 Decode Error") // Token Base64解码错误
|
||||
ErrTokenAuthParseFail = NewError(1304, "Token Authorization Fail") // Token授权解析失败
|
||||
ErrTokenAuthKeyId = NewError(1305, "Token Key:Id Incorrect") // Token密钥ID错误
|
||||
ErrTokenAuthKeyIdentity = NewError(1306, "Token Key:Identity Incorrect") // Token密钥身份错误
|
||||
ErrTokenAuthTokenChanged = NewError(1307, "Token Authorization Changed") // Token授权已变更
|
||||
ErrTokenAuthExpire = NewError(1308, "Token Authorization Expire") // Token授权已过期
|
||||
ErrTokenJsonDecode = NewError(1309, "Token Authorization JSON Decode Error") // Token JSON解码错误
|
||||
ErrTokenJsonEncode = NewError(1310, "Token Authorization JSON Encode Error") // Token JSON编码错误
|
||||
ErrTokenSecretKey = NewError(1311, "Token SecretKey Error") // Token密钥错误
|
||||
ErrTokenSecretKeyNotFound = NewError(1312, "Token SecretKey Not Found") // Token密钥未找到
|
||||
ErrTokenGenerate = NewError(1313, "Generate Token Fail") // 生成令牌失败
|
||||
ErrTokenParse = NewError(1314, "Parse Token Fail") // 解析令牌失败
|
||||
)
|
||||
|
||||
// 基础设施相关错误码,起始码:1500
|
||||
@@ -81,6 +84,7 @@ var (
|
||||
// code: 错误码
|
||||
// msg: 错误消息
|
||||
func NewError(code int, msg string) error {
|
||||
AllErrors[code] = msg
|
||||
return status.New(codes.Code(code), msg).Err()
|
||||
}
|
||||
|
||||
@@ -88,6 +92,7 @@ func NewError(code int, msg string) error {
|
||||
// code: 错误码
|
||||
// msg: 错误消息
|
||||
func ErrFatal(code int, msg string) error {
|
||||
AllErrors[code] = msg
|
||||
return status.New(codes.Code(code), msg).Err()
|
||||
}
|
||||
|
||||
@@ -95,5 +100,15 @@ func ErrFatal(code int, msg string) error {
|
||||
// code: 错误码
|
||||
// msg: 错误消息,会自动转换为大写
|
||||
func ErrNotFound(code int, msg string) error {
|
||||
return status.New(codes.Code(code), strings.ToUpper(msg)).Err()
|
||||
AllErrors[code] = msg
|
||||
return status.New(codes.Code(code), msg).Err()
|
||||
}
|
||||
|
||||
// IsErr 检查错误是否与指定的错误匹配
|
||||
func IsErr(err, target error) bool {
|
||||
return status.Code(err) == status.Code(target)
|
||||
}
|
||||
|
||||
func String(err error, msg string) error {
|
||||
return status.New(status.Code(err), err.Error()+", "+msg).Err()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
@@ -13,7 +15,8 @@ var Response Reply
|
||||
type Reply struct {
|
||||
Code int32 `json:"code"` // 响应码
|
||||
Message string `json:"message"` // 响应消息
|
||||
Result any `json:"result"` // 响应数据
|
||||
Details any `json:"details"` // 响应数据
|
||||
Timeseq int64 `json:"timeseq"` // 时间戳序列
|
||||
}
|
||||
|
||||
// Success 返回成功响应
|
||||
@@ -21,10 +24,11 @@ type Reply struct {
|
||||
// data: 响应数据
|
||||
func (reply *Reply) Success(ctx *gin.Context, data any) {
|
||||
reply.Code = 0
|
||||
reply.Result = data
|
||||
reply.Details = data
|
||||
reply.Message = ""
|
||||
reply.Timeseq = time.Now().UnixMilli()
|
||||
if data == nil {
|
||||
reply.Result = ""
|
||||
reply.Details = ""
|
||||
}
|
||||
ctx.JSON(200, reply)
|
||||
}
|
||||
@@ -34,7 +38,7 @@ func (reply *Reply) Success(ctx *gin.Context, data any) {
|
||||
// err: 错误对象
|
||||
func (reply *Reply) Error(ctx *gin.Context, err error) {
|
||||
reply.Code = 500
|
||||
reply.Result = ""
|
||||
reply.Details = ""
|
||||
// 默认状态码为500
|
||||
e, ok := status.FromError(err)
|
||||
if ok {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -70,7 +71,7 @@ func init() {
|
||||
|
||||
func WatchCheckLicence(licPath, licName string) {
|
||||
utils.SetInterval(func() {
|
||||
if CheckLicence(licPath, licName) == false {
|
||||
if !CheckLicence(licPath, licName) {
|
||||
log.Println("授权文件失效,请重新部署授权文件:", licPath)
|
||||
os.Exit(99)
|
||||
}
|
||||
@@ -81,6 +82,10 @@ func WatchCheckLicence(licPath, licName string) {
|
||||
// --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
func CheckLicence(licPath, licName string) bool {
|
||||
// 加载授权文件
|
||||
if licName == "" {
|
||||
licName = "licence.key"
|
||||
}
|
||||
licPath = filepath.Join(licPath, licName)
|
||||
content, err := LoadLicenceFromFile(licPath)
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -98,11 +103,6 @@ func CheckLicence(licPath, licName string) bool {
|
||||
// --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
// Licence 校验
|
||||
func (l *Licence) VerifyLicence(licName string) bool {
|
||||
// 用于开发环境,为授权公司时跳过验证
|
||||
if l.CompanyName == licName {
|
||||
return true
|
||||
}
|
||||
|
||||
today := StrToInt(time.Now().Format("20060102"))
|
||||
// 机器日期不在授权文件有限期之内 (早于生效日期,或超过有效期)
|
||||
if (today < l.CreateDate) || (today > l.ExpireDate) {
|
||||
@@ -135,7 +135,7 @@ func (l *Licence) ValidMachineCode(code string) bool {
|
||||
// --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
// 加载授权文件
|
||||
func LoadLicenceFromFile(licPath string) (string, error) {
|
||||
key_path := path.Join(licPath, "licence.key")
|
||||
key_path := path.Join(licPath)
|
||||
if utils.PathExists(key_path) {
|
||||
file, err := os.Open(key_path)
|
||||
if err != nil {
|
||||
|
||||
@@ -41,6 +41,23 @@ var (
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// 初始化Logger配置
|
||||
func New(cfg *conf.LogConf) {
|
||||
if cfg == nil {
|
||||
cfg = &conf.LogConf{
|
||||
Name: strings.ToLower(vars.ServiceKey),
|
||||
Level: vars.LogLevel(vars.DEBUG),
|
||||
Dir: "./logs/",
|
||||
Endpoint: "",
|
||||
Console: true,
|
||||
File: true,
|
||||
Remote: false,
|
||||
}
|
||||
}
|
||||
|
||||
InitLogger(cfg)
|
||||
}
|
||||
|
||||
// InitLogger 初始化全局日志器
|
||||
func InitLogger(cfg *conf.LogConf) error {
|
||||
var err error
|
||||
@@ -70,7 +87,7 @@ func NewLogger(cfg *conf.LogConf) (*Logger, error) {
|
||||
multiWriter := io.MultiWriter(consoleWriter, fileWriter)
|
||||
|
||||
logger := &Logger{
|
||||
level: vars.LogLevel(cfg.Level),
|
||||
level: cfg.Level,
|
||||
fileWriter: fileWriter,
|
||||
consoleWriter: consoleWriter,
|
||||
logDir: cfg.Dir,
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/crypto/encipher"
|
||||
"git.apinb.com/bsm-sdk/core/crypto/token"
|
||||
"git.apinb.com/bsm-sdk/core/env"
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -27,19 +27,18 @@ func JwtAuth(time_verify bool) gin.HandlerFunc {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
// 提取Token
|
||||
claims, err := encipher.ParseTokenAes(authHeader)
|
||||
if err != nil || claims == nil {
|
||||
log.Printf("提取token异常:%v\n", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token is required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 检测是否需要验证token时间
|
||||
if time_verify {
|
||||
// 判断时间claims.ExpiresAt
|
||||
if time.Now().Unix() > claims.ExpiresAt {
|
||||
isExpire, err := token.New(env.Runtime.JwtSecretKey).IsExpired(authHeader)
|
||||
if err != nil {
|
||||
log.Println("token解析异常:", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token is required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if isExpire {
|
||||
log.Println("token过期,请重新获取:", "Token has expired")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token has expired"})
|
||||
c.Abort()
|
||||
@@ -47,6 +46,15 @@ func JwtAuth(time_verify bool) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// 提取Token
|
||||
claims, err := token.New(env.Runtime.JwtSecretKey).ParseJwt(authHeader)
|
||||
if err != nil || claims == nil {
|
||||
log.Printf("提取token异常:%v\n", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token is required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 将解析后的 Token 存储到上下文中
|
||||
c.Set("Auth", claims)
|
||||
// 如果 Token 有效,继续处理请求
|
||||
@@ -60,8 +68,8 @@ func JwtAuth(time_verify bool) gin.HandlerFunc {
|
||||
func ParseAuth(c *gin.Context) (*types.JwtClaims, error) {
|
||||
claims, ok := c.Get("Auth")
|
||||
if !ok {
|
||||
log.Printf("获取登录信息异常: %v", errcode.ErrJWTAuthNotFound)
|
||||
return nil, errcode.ErrJWTAuthNotFound
|
||||
log.Printf("获取登录信息异常: %v", errcode.ErrTokenAuthNotFound)
|
||||
return nil, errcode.ErrTokenAuthNotFound
|
||||
}
|
||||
|
||||
json_claims, err := json.Marshal(claims)
|
||||
|
||||
@@ -3,9 +3,9 @@ package service
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/crypto/encipher"
|
||||
"git.apinb.com/bsm-sdk/core/crypto/token"
|
||||
"git.apinb.com/bsm-sdk/core/env"
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/types"
|
||||
"git.apinb.com/bsm-sdk/core/utils"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
@@ -16,19 +16,19 @@ type ParseOptions struct {
|
||||
MustPrivateAllow bool // 是否只允许私有IP访问
|
||||
}
|
||||
|
||||
func ParseMetaCtx(ctx context.Context, opts *ParseOptions) (*types.JwtClaims, error) {
|
||||
func ParseMetaCtx(ctx context.Context, opts *ParseOptions) (*token.Claims, error) {
|
||||
// 解析metada中的信息并验证
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return nil, errcode.ErrJWTAuthNotFound
|
||||
return nil, errcode.ErrTokenAuthNotFound
|
||||
}
|
||||
|
||||
var Authorizations []string = md.Get("authorization")
|
||||
if len(Authorizations) == 0 || Authorizations[0] == "" {
|
||||
return nil, errcode.ErrJWTAuthNotFound
|
||||
return nil, errcode.ErrTokenAuthNotFound
|
||||
}
|
||||
|
||||
claims, err := encipher.ParseTokenAes(Authorizations[0])
|
||||
claims, err := token.New(env.Runtime.JwtSecretKey).ParseJwt(Authorizations[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func ParseMetaCtx(ctx context.Context, opts *ParseOptions) (*types.JwtClaims, er
|
||||
|
||||
}
|
||||
|
||||
func checkRole(claims *types.JwtClaims, roleKey, roleValue string) bool {
|
||||
func checkRole(claims *token.Claims, roleKey, roleValue string) bool {
|
||||
if roleValue == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
345
utils/net.go
345
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
|
||||
}
|
||||
|
||||
457
utils/qrcode.go
Normal file
457
utils/qrcode.go
Normal file
@@ -0,0 +1,457 @@
|
||||
// Package utils 提供通用工具函数
|
||||
//
|
||||
// 二维码生成功能模块
|
||||
//
|
||||
// 本模块提供了完整的二维码生成功能,支持:
|
||||
// - 基础二维码生成(保存为PNG文件)
|
||||
// - 生成字节数组(可用于HTTP响应、数据库存储等)
|
||||
// - Base64编码输出(便于存储和传输)
|
||||
// - Data URL格式(可直接用于HTML <img>标签)
|
||||
// - 自定义配置(尺寸、颜色、纠错级别)
|
||||
// - 带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 <img>标签的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
|
||||
}
|
||||
Reference in New Issue
Block a user