This commit is contained in:
huangjin
2026-06-29 17:34:59 +08:00
commit fe3ad20fe2
271 changed files with 51767 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
// 微信小程序 SDK 封装,使用 silenceper/wechat/v2
package wechat
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"strings"
myconfig "com.sclktx/m/v2/internal/pkg/config"
"github.com/silenceper/wechat/v2"
"github.com/silenceper/wechat/v2/cache"
miniProgram "github.com/silenceper/wechat/v2/miniprogram"
"github.com/silenceper/wechat/v2/miniprogram/auth"
"github.com/silenceper/wechat/v2/miniprogram/subscribe"
wechatConfig "github.com/silenceper/wechat/v2/miniprogram/config"
"github.com/sirupsen/logrus"
)
// MiniProgramClient 微信小程序客户端
type MiniProgramClient struct {
mp *miniProgram.MiniProgram
}
// NewMiniProgramClient 创建小程序客户端
func NewMiniProgramClient(cfg *myconfig.WechatConfig) *MiniProgramClient {
// 开启 wechat SDK 日志(开发阶段使用 DebugLevel生产环境可改为 WarnLevel
logrus.SetOutput(os.Stdout)
logrus.SetLevel(logrus.DebugLevel)
logrus.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
})
wc := wechat.NewWechat()
// 使用内存缓存 access_token生产环境可换 Redis
memoryCache := cache.NewMemory()
mp := wc.GetMiniProgram(&wechatConfig.Config{
AppID: cfg.AppID,
AppSecret: cfg.AppSecret,
Cache: memoryCache,
})
return &MiniProgramClient{mp: mp}
}
// Code2Session 用 wx.login 返回的 code 换取 openid 和 session_key
func (c *MiniProgramClient) Code2Session(code string) (*auth.ResCode2Session, error) {
result, err := c.mp.GetAuth().Code2Session(code)
if err != nil {
return nil, fmt.Errorf("code2session failed: %w", err)
}
if result.ErrCode != 0 {
return nil, fmt.Errorf("code2session error: %d %s", result.ErrCode, result.ErrMsg)
}
return &result, nil
}
// DecryptPhone 解密手机号(需 session_key + encrypted_data + iv
func (c *MiniProgramClient) DecryptPhone(sessionKey, encryptedData, iv string) (string, error) {
phoneInfo, err := c.mp.GetEncryptor().Decrypt(sessionKey, encryptedData, iv)
if err != nil {
return "", fmt.Errorf("decrypt phone failed: %w", err)
}
return phoneInfo.PhoneNumber, nil
}
// GetAccessToken 获取小程序 access_token
func (c *MiniProgramClient) GetAccessToken() (string, error) {
token, err := c.mp.GetContext().GetAccessToken()
if err != nil {
return "", fmt.Errorf("get access_token failed: %w", err)
}
return token, nil
}
// UploadImgResponse 微信上传图片接口返回结构
type UploadImgResponse struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
URL string `json:"url"` // 微信返回的图片 URL
}
// UploadImg 上传图片到微信服务器(作为永久素材)
// imageData: 图片二进制数据
// filename: 文件名
func (c *MiniProgramClient) UploadImg(imageData []byte, filename string) (*UploadImgResponse, error) {
// 获取 access_token
accessToken, err := c.GetAccessToken()
if err != nil {
return nil, err
}
// 构造 multipart/form-data 请求
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// 添加 media 字段(微信要求的字段名)
part, err := writer.CreateFormFile("media", filename)
if err != nil {
return nil, fmt.Errorf("create form file failed: %w", err)
}
if _, err := part.Write(imageData); err != nil {
return nil, fmt.Errorf("write image data failed: %w", err)
}
if err := writer.Close(); err != nil {
return nil, fmt.Errorf("close writer failed: %w", err)
}
// 发送请求
url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=%s", accessToken)
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, fmt.Errorf("create request failed: %w", err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("upload img request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response failed: %w", err)
}
var result UploadImgResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("parse response failed: %w, body: %s", err, string(respBody))
}
if result.ErrCode != 0 {
return nil, fmt.Errorf("wechat upload img error: %d %s", result.ErrCode, result.ErrMsg)
}
// 微信返回的 URL 是 http:// 协议,统一替换为 https://
result.URL = strings.Replace(result.URL, "http://", "https://", 1)
return &result, nil
}
// SendSubscribeMessage 发送微信订阅消息
// openid: 接收用户的 openid
// templateID: 模板 ID
// data: 模板数据key 为模板字段名(如 thing1, date2value 为字段值
func (c *MiniProgramClient) SendSubscribeMessage(openid, templateID string, data map[string]string, page string) error {
msg := &subscribe.Message{
ToUser: openid,
TemplateID: templateID,
Page: page,
Data: make(map[string]*subscribe.DataItem),
}
for k, v := range data {
msg.Data[k] = &subscribe.DataItem{Value: v}
}
err := c.mp.GetSubscribe().Send(msg)
if err != nil {
logrus.WithFields(logrus.Fields{
"openid": openid,
"template_id": templateID,
}).Errorf("微信订阅消息推送失败: %v", err)
} else {
logrus.WithFields(logrus.Fields{
"openid": openid,
"template_id": templateID,
}).Info("微信订阅消息推送成功")
}
return err
}