refactor(utils): 修复密码哈希比较逻辑错误 feat(user): 新增按状态筛选优惠券接口 docs: 添加虚拟发货与任务中心相关文档 fix(wechat): 修正Code2Session上下文传递问题 test: 补充订单折扣与积分转换测试用例 build: 更新配置文件与构建脚本 style: 清理多余的空行与注释
171 lines
5.5 KiB
Go
171 lines
5.5 KiB
Go
package notify
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"bindbox-game/internal/pkg/httpclient"
|
||
)
|
||
|
||
// 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个字符)
|
||
if len(activityName) > 20 {
|
||
activityName = activityName[:17] + "..."
|
||
}
|
||
|
||
// 构建中奖结果描述(phrase类型限制5个汉字以内)
|
||
// 将中奖奖品写入到 resultPhrase
|
||
resultPhrase := strings.Join(rewardNames, ",")
|
||
if len(resultPhrase) > 5 {
|
||
resultPhrase = resultPhrase[:2] + "..."
|
||
}
|
||
|
||
// 温馨提示:固定短语
|
||
warmTips := "已发送到您的货柜"
|
||
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
|
||
}
|