180 lines
5.3 KiB
Go
180 lines
5.3 KiB
Go
// 微信小程序 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, date2),value 为字段值
|
||
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
|
||
}
|