170 lines
5.6 KiB
Go
170 lines
5.6 KiB
Go
package notify
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"bindbox-game/internal/pkg/httpclient"
|
||
pkgutils "bindbox-game/internal/pkg/utils"
|
||
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// WechatNotifyConfig 微信通知配置
|
||
type WechatNotifyConfig struct {
|
||
AppID string
|
||
AppSecret string
|
||
LotteryResultTemplateID string
|
||
}
|
||
|
||
// LotteryResultNotificationRequest 开奖结果通知请求结构
|
||
type LotteryResultNotificationRequest struct {
|
||
Touser string `json:"touser"`
|
||
TemplateID string `json:"template_id"`
|
||
Page string `json:"page"`
|
||
MiniprogramState string `json:"miniprogram_state"`
|
||
Lang string `json:"lang"`
|
||
Data LotteryResultNotificationData `json:"data"`
|
||
}
|
||
|
||
// LotteryResultNotificationData 开奖结果通知数据字段
|
||
// 使用 map 支持动态字段类型,根据模板灵活配置
|
||
type LotteryResultNotificationData map[string]DataValue
|
||
|
||
// DataValue 数据值包装
|
||
type DataValue struct {
|
||
Value string `json:"value"`
|
||
}
|
||
|
||
// LotteryResultNotificationResponse 发送结果响应
|
||
type LotteryResultNotificationResponse struct {
|
||
Errcode int `json:"errcode"`
|
||
Errmsg string `json:"errmsg"`
|
||
}
|
||
|
||
// AccessTokenResponse access_token 响应
|
||
type AccessTokenResponse struct {
|
||
AccessToken string `json:"access_token"`
|
||
ExpiresIn int `json:"expires_in"`
|
||
ErrCode int `json:"errcode,omitempty"`
|
||
ErrMsg string `json:"errmsg,omitempty"`
|
||
}
|
||
|
||
// getAccessToken 获取微信 access_token
|
||
func getAccessToken(ctx context.Context, appID, appSecret string) (string, error) {
|
||
url := "https://api.weixin.qq.com/cgi-bin/token"
|
||
client := httpclient.GetHttpClient()
|
||
resp, err := client.R().
|
||
SetQueryParams(map[string]string{
|
||
"grant_type": "client_credential",
|
||
"appid": appID,
|
||
"secret": appSecret,
|
||
}).
|
||
Get(url)
|
||
if err != nil {
|
||
return "", fmt.Errorf("获取access_token失败: %v", err)
|
||
}
|
||
if resp.StatusCode() != http.StatusOK {
|
||
return "", fmt.Errorf("HTTP请求失败,状态码: %d", resp.StatusCode())
|
||
}
|
||
var tokenResp AccessTokenResponse
|
||
if err := json.Unmarshal(resp.Body(), &tokenResp); err != nil {
|
||
return "", fmt.Errorf("解析access_token响应失败: %v", err)
|
||
}
|
||
if tokenResp.ErrCode != 0 {
|
||
return "", fmt.Errorf("获取access_token失败: errcode=%d, errmsg=%s", tokenResp.ErrCode, tokenResp.ErrMsg)
|
||
}
|
||
if tokenResp.AccessToken == "" {
|
||
return "", fmt.Errorf("获取到的access_token为空")
|
||
}
|
||
return tokenResp.AccessToken, nil
|
||
}
|
||
|
||
// SendLotteryResultNotification 发送开奖结果订阅消息
|
||
// ctx: context
|
||
// cfg: 微信通知配置
|
||
// openid: 用户 openid
|
||
// activityName: 活动名称
|
||
// rewardNames: 中奖奖品列表
|
||
// orderNo: 订单编号
|
||
// drawTime: 开奖时间
|
||
func SendLotteryResultNotification(ctx context.Context, cfg *WechatNotifyConfig, openid string, activityName string, rewardNames []string, orderNo string, drawTime time.Time) error {
|
||
if cfg == nil || cfg.LotteryResultTemplateID == "" {
|
||
fmt.Printf("[开奖通知] 模板ID未配置,跳过发送 openid=%s\n", openid)
|
||
return nil
|
||
}
|
||
if openid == "" {
|
||
fmt.Printf("[开奖通知] openid为空,跳过发送\n")
|
||
return nil
|
||
}
|
||
|
||
// 获取 access_token
|
||
accessToken, err := getAccessToken(ctx, cfg.AppID, cfg.AppSecret)
|
||
if err != nil {
|
||
zap.L().Error("[开奖通知] 获取access_token失败", zap.Error(err), zap.String("openid", openid))
|
||
return err
|
||
}
|
||
// 活动名称限制长度(thing类型不超过20个字符)
|
||
activityName = pkgutils.TruncateRunes(activityName, 20)
|
||
|
||
// 活动结果:展示奖品列表
|
||
rewardsStr := strings.Join(rewardNames, ",")
|
||
if rewardsStr == "" {
|
||
rewardsStr = "无奖励"
|
||
}
|
||
// thing类型限制20字符
|
||
resultVal := pkgutils.TruncateRunes(rewardsStr, 20)
|
||
|
||
// 当前进度:固定为"已发货"
|
||
progress := "已发货"
|
||
|
||
// 使用模板字段:thing6=活动名称, thing8=当前进度, thing9=活动结果
|
||
req := &LotteryResultNotificationRequest{
|
||
Touser: openid,
|
||
TemplateID: cfg.LotteryResultTemplateID,
|
||
Page: "pages/mine/index", // 点击跳转到"我的"页面
|
||
MiniprogramState: "formal", // 正式版
|
||
Lang: "zh_CN",
|
||
Data: LotteryResultNotificationData{
|
||
"thing6": {Value: activityName}, // 活动名称
|
||
"thing8": {Value: progress}, // 当前进度
|
||
"thing9": {Value: resultVal}, // 活动结果
|
||
},
|
||
}
|
||
|
||
zap.L().Info("[开奖通知] 尝试发送", zap.String("openid", openid), zap.String("activity", activityName), zap.Strings("rewards", rewardNames))
|
||
|
||
// 发送请求
|
||
url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=%s", accessToken)
|
||
client := httpclient.GetHttpClient()
|
||
resp, err := client.R().
|
||
SetHeader("Content-Type", "application/json").
|
||
SetBody(req).
|
||
Post(url)
|
||
if err != nil {
|
||
zap.L().Error("[开奖通知] 发送失败", zap.Error(err), zap.String("openid", openid))
|
||
return err
|
||
}
|
||
|
||
var result LotteryResultNotificationResponse
|
||
if err := json.Unmarshal(resp.Body(), &result); err != nil {
|
||
zap.L().Error("[开奖通知] 解析响应失败", zap.Error(err), zap.String("body", string(resp.Body())))
|
||
return err
|
||
}
|
||
|
||
if result.Errcode != 0 {
|
||
// 常见错误码:
|
||
// 43101: 用户拒绝接受消息
|
||
// 47003: 模板参数不准确
|
||
zap.L().Warn("[开奖通知] 发送失败", zap.Int("errcode", result.Errcode), zap.String("errmsg", result.Errmsg), zap.String("openid", openid))
|
||
return fmt.Errorf("发送订阅消息失败: errcode=%d, errmsg=%s", result.Errcode, result.Errmsg)
|
||
}
|
||
|
||
zap.L().Info("[开奖通知] ✅ 发送成功", zap.String("openid", openid))
|
||
return nil
|
||
}
|