feat(wechat): 添加微信小程序二维码生成功能
实现微信小程序二维码生成接口,包括获取access_token和生成二维码的逻辑 添加路由配置和handler处理函数,支持返回Base64编码的二维码图片
This commit is contained in:
parent
713b0e723a
commit
cd2093f594
71
internal/api/wechat/generate_qrcode.go
Normal file
71
internal/api/wechat/generate_qrcode.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package wechat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"mini-chat/internal/code"
|
||||||
|
"mini-chat/internal/pkg/core"
|
||||||
|
"mini-chat/internal/pkg/wechat"
|
||||||
|
)
|
||||||
|
|
||||||
|
type generateQRCodeRequest struct {
|
||||||
|
AppID string `json:"app_id" binding:"required"` // 微信小程序 AppID
|
||||||
|
AppSecret string `json:"app_secret" binding:"required"` // 微信小程序 AppSecret
|
||||||
|
Path string `json:"path" binding:"required"` // 小程序页面路径
|
||||||
|
}
|
||||||
|
|
||||||
|
type generateQRCodeResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data string `json:"data"` // Base64 编码的图片数据
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateQRCode 生成微信小程序二维码
|
||||||
|
// @Summary 生成微信小程序二维码
|
||||||
|
// @Description 根据 AppID、AppSecret 和页面路径生成微信小程序二维码
|
||||||
|
// @Tags 微信
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body generateQRCodeRequest true "请求参数"
|
||||||
|
// @Success 200 {object} generateQRCodeResponse
|
||||||
|
// @Failure 400 {object} code.Failure
|
||||||
|
// @Failure 500 {object} code.Failure
|
||||||
|
// @Router /api/wechat/qrcode [post]
|
||||||
|
func (h *handler) GenerateQRCode() core.HandlerFunc {
|
||||||
|
return func(ctx core.Context) {
|
||||||
|
req := new(generateQRCodeRequest)
|
||||||
|
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||||
|
ctx.AbortWithError(core.Error(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
code.ParamBindError,
|
||||||
|
code.Text(code.ParamBindError),
|
||||||
|
))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用微信小程序二维码生成函数
|
||||||
|
imageData, err := wechat.GenerateQRCode(req.AppID, req.AppSecret, req.Path)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error(fmt.Sprintf("生成微信小程序二维码失败: %s", err.Error()))
|
||||||
|
ctx.AbortWithError(core.Error(
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
code.ServerError,
|
||||||
|
err.Error(),
|
||||||
|
))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将图片数据转换为 Base64 编码
|
||||||
|
base64Data := base64.StdEncoding.EncodeToString(imageData)
|
||||||
|
|
||||||
|
res := &generateQRCodeResponse{
|
||||||
|
Success: true,
|
||||||
|
Message: "二维码生成成功",
|
||||||
|
Data: base64Data,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Payload(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
internal/api/wechat/wechat.go
Normal file
21
internal/api/wechat/wechat.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package wechat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mini-chat/internal/pkg/logger"
|
||||||
|
"mini-chat/internal/repository/mysql"
|
||||||
|
"mini-chat/internal/repository/mysql/dao"
|
||||||
|
)
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
logger logger.CustomLogger
|
||||||
|
writeDB *dao.Query
|
||||||
|
readDB *dao.Query
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
|
||||||
|
return &handler{
|
||||||
|
logger: logger,
|
||||||
|
writeDB: dao.Use(db.GetDbW()),
|
||||||
|
readDB: dao.Use(db.GetDbR()),
|
||||||
|
}
|
||||||
|
}
|
||||||
261
internal/pkg/wechat/qrcode.go
Normal file
261
internal/pkg/wechat/qrcode.go
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
package wechat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"mini-chat/internal/pkg/core"
|
||||||
|
"mini-chat/internal/pkg/httpclient"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccessTokenRequest 获取 access_token 请求参数
|
||||||
|
type AccessTokenRequest struct {
|
||||||
|
AppID string `json:"appid"`
|
||||||
|
AppSecret string `json:"secret"`
|
||||||
|
GrantType string `json:"grant_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccessTokenResponse 获取 access_token 响应
|
||||||
|
type AccessTokenResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
ErrCode int `json:"errcode,omitempty"`
|
||||||
|
ErrMsg string `json:"errmsg,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenCache access_token 缓存结构
|
||||||
|
type TokenCache struct {
|
||||||
|
Token string
|
||||||
|
ExpiresAt time.Time
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局 token 缓存
|
||||||
|
var globalTokenCache = &TokenCache{}
|
||||||
|
|
||||||
|
// WechatConfig 微信配置
|
||||||
|
type WechatConfig struct {
|
||||||
|
AppID string
|
||||||
|
AppSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
// QRCodeRequest 获取小程序码请求参数
|
||||||
|
type QRCodeRequest struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Width int `json:"width,omitempty"`
|
||||||
|
AutoColor bool `json:"auto_color,omitempty"`
|
||||||
|
LineColor *LineColor `json:"line_color,omitempty"`
|
||||||
|
IsHyaline bool `json:"is_hyaline,omitempty"`
|
||||||
|
EnvVersion string `json:"release,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineColor struct {
|
||||||
|
R int `json:"r"`
|
||||||
|
G int `json:"g"`
|
||||||
|
B int `json:"b"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QRCodeResponse 获取小程序码响应
|
||||||
|
type QRCodeResponse struct {
|
||||||
|
Buffer []byte `json:"-"`
|
||||||
|
ErrCode int `json:"errcode,omitempty"`
|
||||||
|
ErrMsg string `json:"errmsg,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QRCodeError 微信API错误响应
|
||||||
|
type QRCodeError struct {
|
||||||
|
ErrCode int `json:"errcode"`
|
||||||
|
ErrMsg string `json:"errmsg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *QRCodeError) Error() string {
|
||||||
|
return fmt.Sprintf("微信API错误: errcode=%d, errmsg=%s", e.ErrCode, e.ErrMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccessToken 获取微信 access_token
|
||||||
|
func GetAccessToken(ctx core.StdContext, config *WechatConfig) (string, error) {
|
||||||
|
if config == nil {
|
||||||
|
return "", fmt.Errorf("微信配置不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.AppID == "" {
|
||||||
|
return "", fmt.Errorf("AppID 不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.AppSecret == "" {
|
||||||
|
return "", fmt.Errorf("AppSecret 不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
globalTokenCache.mutex.RLock()
|
||||||
|
if globalTokenCache.Token != "" && time.Now().Before(globalTokenCache.ExpiresAt) {
|
||||||
|
token := globalTokenCache.Token
|
||||||
|
globalTokenCache.mutex.RUnlock()
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
globalTokenCache.mutex.RUnlock()
|
||||||
|
|
||||||
|
// 缓存过期或不存在,重新获取
|
||||||
|
globalTokenCache.mutex.Lock()
|
||||||
|
defer globalTokenCache.mutex.Unlock()
|
||||||
|
|
||||||
|
// 双重检查,防止并发情况下重复请求
|
||||||
|
if globalTokenCache.Token != "" && time.Now().Before(globalTokenCache.ExpiresAt) {
|
||||||
|
return globalTokenCache.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求URL
|
||||||
|
url := "https://api.weixin.qq.com/cgi-bin/token"
|
||||||
|
|
||||||
|
// 发送HTTP请求
|
||||||
|
client := httpclient.GetHttpClientWithContext(ctx)
|
||||||
|
resp, err := client.R().
|
||||||
|
SetQueryParams(map[string]string{
|
||||||
|
"grant_type": "client_credential",
|
||||||
|
"appid": config.AppID,
|
||||||
|
"secret": config.AppSecret,
|
||||||
|
}).
|
||||||
|
Get(url)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("获取access_token失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode() != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("HTTP请求失败,状态码: %d", resp.StatusCode())
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenResp AccessTokenResponse
|
||||||
|
if err := json.Unmarshal(resp.Body(), &tokenResp); err != nil {
|
||||||
|
return "", fmt.Errorf("解析access_token响应失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenResp.ErrCode != 0 {
|
||||||
|
return "", fmt.Errorf("获取access_token失败: errcode=%d, errmsg=%s", tokenResp.ErrCode, tokenResp.ErrMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenResp.AccessToken == "" {
|
||||||
|
return "", fmt.Errorf("获取到的access_token为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
expiresAt := time.Now().Add(time.Duration(tokenResp.ExpiresIn-300) * time.Second)
|
||||||
|
globalTokenCache.Token = tokenResp.AccessToken
|
||||||
|
globalTokenCache.ExpiresAt = expiresAt
|
||||||
|
|
||||||
|
return tokenResp.AccessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQRCodeWithConfig 通过微信配置获取小程序码(自动获取access_token)
|
||||||
|
// config: 微信配置(AppID 和 AppSecret)
|
||||||
|
// request: 请求参数
|
||||||
|
// 返回: 成功时返回图片二进制数据,失败时返回错误
|
||||||
|
func GetQRCodeWithConfig(ctx core.StdContext, config *WechatConfig, request *QRCodeRequest) (*QRCodeResponse, error) {
|
||||||
|
// 自动获取 access_token
|
||||||
|
accessToken, err := GetAccessToken(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取access_token失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetQRCode(ctx, accessToken, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQRCode 获取小程序码
|
||||||
|
// accessToken: 接口调用凭证
|
||||||
|
// request: 请求参数
|
||||||
|
// 返回: 成功时返回图片二进制数据,失败时返回错误
|
||||||
|
func GetQRCode(ctx core.StdContext, accessToken string, request *QRCodeRequest) (*QRCodeResponse, error) {
|
||||||
|
if accessToken == "" {
|
||||||
|
return nil, fmt.Errorf("access_token 不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
if request == nil {
|
||||||
|
return nil, fmt.Errorf("请求参数不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.Path == "" {
|
||||||
|
return nil, fmt.Errorf("path 参数不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(request.Path) > 1024 {
|
||||||
|
return nil, fmt.Errorf("path 参数长度不能超过1024个字符")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认值
|
||||||
|
if request.Width == 0 {
|
||||||
|
request.Width = 430
|
||||||
|
}
|
||||||
|
if request.Width < 280 {
|
||||||
|
request.Width = 280
|
||||||
|
}
|
||||||
|
if request.Width > 1280 {
|
||||||
|
request.Width = 1280
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://api.weixin.qq.com/wxa/getwxacode?access_token=%s", accessToken)
|
||||||
|
|
||||||
|
requestBody, err := json.Marshal(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("序列化请求参数失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := httpclient.GetHttpClientWithContext(ctx)
|
||||||
|
resp, err := client.R().
|
||||||
|
SetHeader("Content-Type", "application/json").
|
||||||
|
SetBody(requestBody).
|
||||||
|
Post(url)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("发送HTTP请求失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode() != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("HTTP请求失败,状态码: %d", resp.StatusCode())
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBody := resp.Body()
|
||||||
|
|
||||||
|
if isJSONResponse(responseBody) {
|
||||||
|
var qrError QRCodeError
|
||||||
|
if err := json.Unmarshal(responseBody, &qrError); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析错误响应失败: %v", err)
|
||||||
|
}
|
||||||
|
return nil, &qrError
|
||||||
|
}
|
||||||
|
|
||||||
|
return &QRCodeResponse{
|
||||||
|
Buffer: responseBody,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isJSONResponse(data []byte) bool {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmed := bytes.TrimSpace(data)
|
||||||
|
return len(trimmed) > 0 && trimmed[0] == '{'
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateQRCode 最简化的小程序码生成接口
|
||||||
|
func GenerateQRCode(appID, appSecret, path string) ([]byte, error) {
|
||||||
|
ctx := core.StdContext{}
|
||||||
|
|
||||||
|
config := &WechatConfig{
|
||||||
|
AppID: appID,
|
||||||
|
AppSecret: appSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
request := &QRCodeRequest{
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := GetQRCodeWithConfig(ctx, config, request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Buffer, nil
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"mini-chat/internal/api/keyword"
|
"mini-chat/internal/api/keyword"
|
||||||
"mini-chat/internal/api/message"
|
"mini-chat/internal/api/message"
|
||||||
"mini-chat/internal/api/upload"
|
"mini-chat/internal/api/upload"
|
||||||
|
"mini-chat/internal/api/wechat"
|
||||||
"mini-chat/internal/cron"
|
"mini-chat/internal/cron"
|
||||||
"mini-chat/internal/dblogger"
|
"mini-chat/internal/dblogger"
|
||||||
"mini-chat/internal/pkg/core"
|
"mini-chat/internal/pkg/core"
|
||||||
@ -47,6 +48,7 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo, cron cron.Server) (co
|
|||||||
uploadHandler := upload.New(logger, db)
|
uploadHandler := upload.New(logger, db)
|
||||||
keywordHandler := keyword.New(logger, db)
|
keywordHandler := keyword.New(logger, db)
|
||||||
messageHandler := message.New(logger, db)
|
messageHandler := message.New(logger, db)
|
||||||
|
wechatHandler := wechat.New(logger, db)
|
||||||
|
|
||||||
// 管理端非认证接口路由组
|
// 管理端非认证接口路由组
|
||||||
adminNonAuthApiRouter := mux.Group("/admin")
|
adminNonAuthApiRouter := mux.Group("/admin")
|
||||||
@ -66,6 +68,12 @@ func NewHTTPMux(logger logger.CustomLogger, db mysql.Repo, cron cron.Server) (co
|
|||||||
appNonAuthApiRouter.POST("/send_message", messageHandler.UserSendMessage()) // 发送消息
|
appNonAuthApiRouter.POST("/send_message", messageHandler.UserSendMessage()) // 发送消息
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 微信 API 路由组
|
||||||
|
wechatApiRouter := mux.Group("/api/wechat")
|
||||||
|
{
|
||||||
|
wechatApiRouter.POST("/qrcode", wechatHandler.GenerateQRCode()) // 生成微信小程序二维码(返回 Base64)
|
||||||
|
}
|
||||||
|
|
||||||
// 管理端认证接口路由组
|
// 管理端认证接口路由组
|
||||||
adminAuthApiRouter := mux.Group("/admin", core.WrapAuthHandler(interceptorHandler.AdminTokenAuthVerify))
|
adminAuthApiRouter := mux.Group("/admin", core.WrapAuthHandler(interceptorHandler.AdminTokenAuthVerify))
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user