// 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 }