新增二维码功能模块:qrcode.go;

为http请求添加超时
This commit is contained in:
zhaoxiaorong
2025-11-03 15:27:12 +08:00
parent 3271453878
commit 2c5aab84da
5 changed files with 1311 additions and 158 deletions

View File

@@ -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
View 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
}