Files
silk/server-go/internal/service/wechat.go
T

214 lines
5.3 KiB
Go
Raw 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.
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sync"
"time"
)
const wechatAPIBase = "https://api.weixin.qq.com"
// WechatTemplateKey 风险等级 → 订阅消息场景键(green 不推送)
func WechatTemplateKey(level string) string {
switch level {
case "yellow", "orange", "red":
return "inspection"
default:
return ""
}
}
// IsAuthorized 判断授权模板列表是否包含场景键
func IsAuthorized(authorized []string, key string) bool {
for _, k := range authorized {
if k == key {
return true
}
}
return false
}
// levelZh 风险等级中文
func levelZh(level string) string {
switch level {
case "green":
return "绿"
case "yellow":
return "黄"
case "orange":
return "橙"
case "red":
return "红"
default:
return level
}
}
// BuildSubscribeData 构造订阅消息 datathing1=风险等级 thing2=评分;字段名需与微信模板字段一致)
func BuildSubscribeData(level string, score float64) map[string]map[string]string {
return map[string]map[string]string{
"thing1": {"value": levelZh(level)},
"thing2": {"value": fmt.Sprintf("%.0f分", score)},
}
}
// WechatService 微信小程序订阅消息服务(骨架;未配置时 Configured()=false,调用返回明确错误)
type WechatService struct {
appID string
secret string
baseURL string
accessToken string
tokenExpire time.Time
mu sync.Mutex
httpClient *http.Client
}
// NewWechatService 创建微信服务
func NewWechatService(appID, secret string) *WechatService {
return &WechatService{
appID: appID,
secret: secret,
baseURL: wechatAPIBase,
httpClient: &http.Client{Timeout: 15 * time.Second},
}
}
// Configured 是否已配置 AppID/Secret
func (s *WechatService) Configured() bool {
return s.appID != "" && s.secret != ""
}
// Code2Session 用 wx.login 的 code 换取 openid
func (s *WechatService) Code2Session(ctx context.Context, code string) (string, error) {
if !s.Configured() {
return "", fmt.Errorf("微信未配置(WECHAT_APPID/WECHAT_SECRET")
}
u := s.baseURL + "/sns/jscode2session?appid=" + url.QueryEscape(s.appID) +
"&secret=" + url.QueryEscape(s.secret) +
"&js_code=" + url.QueryEscape(code) + "&grant_type=authorization_code"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return "", err
}
resp, err := s.httpClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var out struct {
OpenID string `json:"openid"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
if err := json.Unmarshal(body, &out); err != nil {
return "", err
}
if out.ErrCode != 0 {
return "", fmt.Errorf("微信 code2session 失败 (%d): %s", out.ErrCode, out.ErrMsg)
}
return out.OpenID, nil
}
// getAccessToken 获取并缓存 access_token(提前 60 秒过期)
func (s *WechatService) getAccessToken(ctx context.Context) (string, error) {
s.mu.Lock()
if s.accessToken != "" && time.Now().Before(s.tokenExpire) {
tok := s.accessToken
s.mu.Unlock()
return tok, nil
}
s.mu.Unlock()
if !s.Configured() {
return "", fmt.Errorf("微信未配置(WECHAT_APPID/WECHAT_SECRET")
}
u := s.baseURL + "/cgi-bin/token?grant_type=client_credential&appid=" +
url.QueryEscape(s.appID) + "&secret=" + url.QueryEscape(s.secret)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
if err != nil {
return "", err
}
resp, err := s.httpClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var out struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
if err := json.Unmarshal(body, &out); err != nil {
return "", err
}
if out.AccessToken == "" {
return "", fmt.Errorf("微信 access_token 获取失败 (%d): %s", out.ErrCode, out.ErrMsg)
}
s.mu.Lock()
s.accessToken = out.AccessToken
s.tokenExpire = time.Now().Add(time.Duration(out.ExpiresIn-60) * time.Second)
s.mu.Unlock()
return out.AccessToken, nil
}
// SendSubscribe 发送订阅消息
func (s *WechatService) SendSubscribe(ctx context.Context, openid, templateID string, data map[string]map[string]string, page string) error {
token, err := s.getAccessToken(ctx)
if err != nil {
return err
}
payload := map[string]any{
"touser": openid,
"template_id": templateID,
"data": data,
}
if page != "" {
payload["page"] = page
}
raw, err := json.Marshal(payload)
if err != nil {
return err
}
u := s.baseURL + "/cgi-bin/message/subscribe/send?access_token=" + url.QueryEscape(token)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewReader(raw))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var out struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
if err := json.Unmarshal(body, &out); err != nil {
return err
}
if out.ErrCode != 0 {
return fmt.Errorf("微信订阅消息发送失败 (%d): %s", out.ErrCode, out.ErrMsg)
}
return nil
}