package notify import ( "context" "encoding/json" "fmt" "net/http" "strings" "time" "bindbox-game/internal/pkg/httpclient" pkgutils "bindbox-game/internal/pkg/utils" ) // 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 开奖结果通知数据字段 // 根据微信订阅消息模板字段定义 // thing1: 活动名称, phrase3: 中奖结果, thing4: 温馨提示 type LotteryResultNotificationData struct { Thing1 DataValue `json:"thing1"` // 活动名称 Phrase3 DataValue `json:"phrase3"` // 中奖结果 Thing4 DataValue `json:"thing4"` // 温馨提示 } // 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 { fmt.Printf("[开奖通知] 获取access_token失败: %v\n", err) return err } // 活动名称限制长度(thing类型不超过20个字符) activityName = pkgutils.TruncateRunes(activityName, 20) // 构建中奖结果描述(phrase类型限制5个汉字以内) // 由于奖品名称通常较长,phrase3 放不下,改为固定文案 "恭喜中奖" // 将奖品名称放入 Thing4 (温馨提示),限制 20 字符 resultPhrase := "恭喜中奖" rewardsStr := strings.Join(rewardNames, ",") warmTips := pkgutils.TruncateRunes(rewardsStr, 20) req := &LotteryResultNotificationRequest{ Touser: openid, TemplateID: cfg.LotteryResultTemplateID, Page: "pages/mine/index", // 点击跳转到"我的"页面 MiniprogramState: "formal", // 正式版 Lang: "zh_CN", Data: LotteryResultNotificationData{ Thing1: DataValue{Value: activityName}, // 活动名称 Phrase3: DataValue{Value: resultPhrase}, // 中奖结果 Thing4: DataValue{Value: warmTips}, // 温馨提示(中奖奖品) }, } fmt.Printf("[开奖通知] 尝试发送 openid=%s activity=%s rewards=%v\n", openid, activityName, 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 { fmt.Printf("[开奖通知] 发送失败: %v\n", err) return err } var result LotteryResultNotificationResponse if err := json.Unmarshal(resp.Body(), &result); err != nil { fmt.Printf("[开奖通知] 解析响应失败: %v\n", err) return err } if result.Errcode != 0 { // 常见错误码: // 43101: 用户拒绝接受消息 // 47003: 模板参数不准确 fmt.Printf("[开奖通知] 发送失败 errcode=%d errmsg=%s\n", result.Errcode, result.Errmsg) return fmt.Errorf("发送订阅消息失败: errcode=%d, errmsg=%s", result.Errcode, result.Errmsg) } fmt.Printf("[开奖通知] ✅ 发送成功 openid=%s\n", openid) return nil }