Files
huangjin fe3ad20fe2 'init'
2026-06-29 17:34:59 +08:00

180 lines
5.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 微信小程序 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
}