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 }