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