feat: analysis gen qrcode

This commit is contained in:
zzw
2026-10-13 11:05:09 +08:00
parent 10b58a7cab
commit 6fa185003d
24 changed files with 671 additions and 183 deletions
+22 -1
View File
@@ -1,8 +1,13 @@
package auth
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"time"
"lone-services/pkg/redis"
"lone-services/pkg/utils"
jsoniter "github.com/json-iterator/go"
@@ -17,10 +22,21 @@ type AccessTokenResult struct {
ErrMsg string `json:"errmsg"`
}
func (c *Client) AccessToken() (*AccessTokenResult, error) {
func (c *Client) AccessToken(ctx context.Context) (*AccessTokenResult, error) {
if c == nil || c.AppID == utils.StringEmpty || c.Secret == utils.StringEmpty {
return nil, fmt.Errorf("wechat app config missing")
}
if ctx == nil {
ctx = context.Background()
}
secretMd5 := md5.Sum([]byte(c.Secret))
key := fmt.Sprintf("wechat:access_token:%s:%s", c.AppID, hex.EncodeToString(secretMd5[:])[:8])
token, err := redis.Client.Get(ctx, key).Result()
if err == nil && token != utils.StringEmpty {
return &AccessTokenResult{AccessToken: token}, nil
}
body, err := utils.Curl(accessTokenURL).SetData(map[string]string{
"grant_type": "client_credential",
@@ -41,5 +57,10 @@ func (c *Client) AccessToken() (*AccessTokenResult, error) {
if result.AccessToken == utils.StringEmpty {
return nil, fmt.Errorf("wechat access_token empty")
}
expire := time.Duration(result.ExpiresIn-300) * time.Second
if expire > 0 {
_ = redis.Client.Set(ctx, key, result.AccessToken, expire).Err()
}
return &result, nil
}
+13
View File
@@ -0,0 +1,13 @@
package qrcode
type Client struct {
AppID string
Secret string
}
func NewClient(appID, secret string) *Client {
return &Client{
AppID: appID,
Secret: secret,
}
}
+33
View File
@@ -0,0 +1,33 @@
package qrcode
type LineColor struct {
R int `json:"r"`
G int `json:"g"`
B int `json:"b"`
}
type UnlimitedRequest struct {
Scene string `json:"scene"`
Page string `json:"page,omitempty"`
CheckPath *bool `json:"check_path,omitempty"`
EnvVersion string `json:"env_version,omitempty"` // release / trial / develop
Width int `json:"width,omitempty"`
AutoColor bool `json:"auto_color,omitempty"`
LineColor *LineColor `json:"line_color,omitempty"`
IsHyaline bool `json:"is_hyaline,omitempty"`
}
// WxaCodeRequest 有数量限制的小程序码(getwxacode),path 可带 query。
type WxaCodeRequest struct {
Path string `json:"path"`
EnvVersion string `json:"env_version,omitempty"`
Width int `json:"width,omitempty"`
AutoColor bool `json:"auto_color,omitempty"`
LineColor *LineColor `json:"line_color,omitempty"`
IsHyaline bool `json:"is_hyaline,omitempty"`
}
type apiErrorBody struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
+88
View File
@@ -0,0 +1,88 @@
package qrcode
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"lone-services/pkg/utils"
"lone-services/pkg/wechat/auth"
)
const unlimitedURL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit"
func (c *Client) GetUnlimited(accessToken string, req *UnlimitedRequest) ([]byte, error) {
if accessToken == utils.StringEmpty {
return nil, fmt.Errorf("wechat access_token empty")
}
if req == nil || req.Scene == utils.StringEmpty {
return nil, fmt.Errorf("wechat qrcode scene empty")
}
payload, err := json.Marshal(req)
if err != nil {
return nil, err
}
url := fmt.Sprintf("%s?access_token=%s", unlimitedURL, accessToken)
httpClient := &http.Client{Timeout: 15 * time.Second}
resp, err := httpClient.Post(url, "application/json", bytes.NewReader(payload))
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("wechat qrcode http status %d", resp.StatusCode)
}
if err := parseImageOrError(body); err != nil {
return nil, err
}
return body, nil
}
func (c *Client) CreateUnlimited(ctx context.Context, req *UnlimitedRequest) ([]byte, error) {
token, err := auth.NewClient(c.AppID, c.Secret).AccessToken(ctx)
if err != nil {
return nil, err
}
return c.GetUnlimited(token.AccessToken, req)
}
func (c *Client) CreateUnlimitedBase64(ctx context.Context, req *UnlimitedRequest) (string, error) {
img, err := c.CreateUnlimited(ctx, req)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(img), nil
}
func parseImageOrError(body []byte) error {
trim := bytes.TrimSpace(body)
if len(trim) == 0 {
return fmt.Errorf("wechat qrcode empty response")
}
if trim[0] == '{' || strings.HasPrefix(string(trim), "{\"errcode\"") {
var errBody apiErrorBody
if err := json.Unmarshal(trim, &errBody); err != nil {
return fmt.Errorf("wechat qrcode invalid error body: %w", err)
}
if errBody.ErrCode != utils.NumberZero {
return errors.New(errBody.ErrMsg)
}
return fmt.Errorf("wechat qrcode unexpected json response")
}
return nil
}
+75
View File
@@ -0,0 +1,75 @@
package qrcode
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
"lone-services/pkg/utils"
"lone-services/pkg/wechat/auth"
)
// 小程序码(有数量限制):https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/qr-code/getQRCode.html
const wxaCodeURL = "https://api.weixin.qq.com/wxa/getwxacode"
// BuildPath 拼小程序 path,如 pagesB/userLogin/index?session_id=xxx
func BuildPath(page string, params map[string]string) string {
if len(params) == 0 {
return page
}
query := url.Values{}
for k, v := range params {
query.Add(k, v)
}
return page + "?" + query.Encode()
}
// GetWxaCode 使用已有 access_token 生成有限小程序码,成功返回图片二进制。
func (c *Client) GetWxaCode(accessToken string, req *WxaCodeRequest) ([]byte, error) {
if accessToken == utils.StringEmpty {
return nil, fmt.Errorf("wechat access_token empty")
}
if req == nil || req.Path == utils.StringEmpty {
return nil, fmt.Errorf("wechat qrcode path empty")
}
payload, err := json.Marshal(req)
if err != nil {
return nil, err
}
apiURL := fmt.Sprintf("%s?access_token=%s", wxaCodeURL, accessToken)
httpClient := &http.Client{Timeout: 15 * time.Second}
resp, err := httpClient.Post(apiURL, "application/json", bytes.NewReader(payload))
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("wechat qrcode http status %d", resp.StatusCode)
}
if err := parseImageOrError(body); err != nil {
return nil, err
}
return body, nil
}
// CreateWxaCode 通过 auth 拉取 access_token 后生成有限小程序码。
func (c *Client) CreateWxaCode(ctx context.Context, req *WxaCodeRequest) ([]byte, error) {
token, err := auth.NewClient(c.AppID, c.Secret).AccessToken(ctx)
if err != nil {
return nil, err
}
return c.GetWxaCode(token.AccessToken, req)
}