fix: 修复微信通知字段截断导致的编码错误 feat: 添加有效邀请相关字段和任务中心常量 refactor: 重构一番赏奖品格位逻辑 perf: 优化道具卡列表聚合显示 docs: 更新项目说明文档和API文档 test: 添加字符串截断工具测试
169 lines
5.6 KiB
Go
169 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"
|
||
)
|
||
|
||
// 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
|
||
}
|