优化小程序

This commit is contained in:
hero920103 committed 2026-10-31 01:21:55 +08:00
1 parent 2e2b837a20
commit 727319a6eb
24 files changed
+1509 -200

No files matched your search

+80
View File
@@ -2,11 +2,14 @@ package wechatmini
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
@@ -138,6 +141,65 @@ func GetPhoneNumber(cfg *Config, phoneCode string) (*PhoneNumber, error) {
}, nil
}
// ============================ 小程序码 ============================
// CreateUnlimitedQRCode 生成小程序码(wxa/getwxacodeunlimited,数量无限制),
// 返回图片 base64 data URL,可直接用于 <img src>。
//
// scene 会话标识,扫码后作为 options.scene 传给目标页面,1-32 个字符
// page 小程序页面路径(如 pages/register/confirm),必须已发布,不带前导 /
// envVersion release(正式版,默认)/ trial(体验版)/ develop(开发版)
func CreateUnlimitedQRCode(cfg *Config, scene, page, envVersion string) (string, error) {
if cfg == nil || cfg.AppID == "" || cfg.AppSecret == "" {
return "", ErrNotConfigured
}
page = strings.TrimPrefix(strings.TrimSpace(page), "/")
if page == "" {
return "", errors.New("未配置小程序码页面路径")
}
scene = strings.TrimSpace(scene)
if scene == "" || len(scene) > 32 {
return "", errors.New("小程序码 scene 无效(需 1-32 个字符)")
}
envVersion = strings.TrimSpace(envVersion)
if envVersion == "" {
envVersion = "release"
}
token, err := getAccessToken(cfg)
if err != nil {
return "", err
}
payload := map[string]interface{}{
"scene": scene,
"page": page,
"env_version": envVersion,
"width": 280,
}
// 体验版 / 开发版下该页面尚未发布到正式版,必须关闭路径校验,
// 否则微信返回 40066 invalid url(正式版可保留默认校验)
if envVersion != "release" {
payload["check_path"] = false
}
api := fmt.Sprintf("%s/wxa/getwxacodeunlimited?access_token=%s", apiBase, url.QueryEscape(token))
body, contentType, err := httpPostBinary(api, payload)
if err != nil {
return "", err
}
// 失败时微信仍返回 200,但响应体是 JSON 错误而非图片
if strings.HasPrefix(strings.TrimSpace(string(body)), "{") {
var resp struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
_ = json.Unmarshal(body, &resp)
return "", &APIError{API: "生成小程序码", Code: resp.ErrCode, Msg: resp.ErrMsg}
}
if contentType == "" {
contentType = "image/jpeg"
}
return "data:" + contentType + ";base64," + base64.StdEncoding.EncodeToString(body), nil
}
// ============================ access_token 缓存 ============================
type tokenEntry struct {
@@ -226,6 +288,24 @@ func httpPostJSON(api string, payload interface{}, out interface{}) error {
return nil
}
// httpPostBinary POST JSON 并返回原始响应体与 Content-Type(小程序码接口返回图片流)
func httpPostBinary(api string, payload interface{}) ([]byte, string, error) {
bs, err := json.Marshal(payload)
if err != nil {
return nil, "", err
}
resp, err := httpClient.Post(api, "application/json; charset=utf-8", bytes.NewReader(bs))
if err != nil {
return nil, "", fmt.Errorf("请求微信接口失败: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, "", fmt.Errorf("读取微信响应失败: %w", err)
}
return body, resp.Header.Get("Content-Type"), nil
}
func truncate(s string, n int) string {
if len(s) <= n {
return s