小程序一些功能更新
This commit is contained in:
101
internal/logic/wxmsg.go
Normal file
101
internal/logic/wxmsg.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 订阅消息发送(笔记定时提醒)
|
||||
//
|
||||
// 依据:PRD-02 §4.1.2、架构 §8.3。
|
||||
// - access_token 复用 xpay.go 的 wxAccessToken()(stable_token + 缓存),不重复造;
|
||||
// - 模板 ID 走配置 wx.subscribe.noteRemindTemplateId;
|
||||
// - ★ 模板 ID 为空 → 不发送、打 Warning 日志、正常返回(降级),绝不 panic。
|
||||
// ============================================================================
|
||||
|
||||
// noteSubscribeSendPath 订阅消息发送接口路径(B 端接口,需 access_token)
|
||||
const noteSubscribeSendPath = "/cgi-bin/message/subscribe/send"
|
||||
|
||||
// NoteRemindSendReq 一条提醒的推送参数
|
||||
type NoteRemindSendReq struct {
|
||||
NoteId int64
|
||||
Openid string
|
||||
Title string
|
||||
Content string
|
||||
Time string
|
||||
}
|
||||
|
||||
// RemindTemplateId 读取订阅消息模板 ID;未配置返回空串。
|
||||
func RemindTemplateId(ctx context.Context) string {
|
||||
return g.Cfg().MustGet(ctx, "wx.subscribe.noteRemindTemplateId", "").String()
|
||||
}
|
||||
|
||||
// SendNoteRemind 发送笔记提醒;从配置解析模板 ID 后交给底层发送。
|
||||
// 模板未配置时底层降级(返回 nil,不 panic)。
|
||||
func SendNoteRemind(ctx context.Context, req NoteRemindSendReq) error {
|
||||
return sendNoteRemindWithTemplate(ctx, req, RemindTemplateId(ctx))
|
||||
}
|
||||
|
||||
// sendNoteRemindWithTemplate 以指定模板 ID 发送订阅消息。
|
||||
//
|
||||
// tplId 为空是**合法的降级路径**(PRD-02 R6-12):仅告警日志并正常返回 nil,
|
||||
// 由调用方(remind.go)把笔记置为「发送失败/未发送」并保留站内提醒标记。
|
||||
// 单独抽出该函数是为了让降级路径可被单元测试直接覆盖(不依赖真实微信环境 / DB)。
|
||||
func sendNoteRemindWithTemplate(ctx context.Context, req NoteRemindSendReq, tplId string) error {
|
||||
if tplId == "" {
|
||||
g.Log().Warningf(ctx,
|
||||
"[note-remind] 未配置订阅消息模板 wx.subscribe.noteRemindTemplateId,降级为站内提醒 noteId=%d", req.NoteId)
|
||||
return nil
|
||||
}
|
||||
if req.Openid == "" {
|
||||
return gerror.New("用户 openid 为空,无法发送订阅消息")
|
||||
}
|
||||
token, err := wxAccessToken(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 模板字段键需与 MP 后台创建的模板一致:thing1 标题 / time2 提醒时间 / thing3 正文摘要。
|
||||
// 该映射属需环境化的集成参数(架构 §十 M1),上线前需与模板对齐。
|
||||
data := g.Map{
|
||||
"thing1": g.Map{"value": clampRunes(req.Title, 20)},
|
||||
"time2": g.Map{"value": req.Time},
|
||||
"thing3": g.Map{"value": clampRunes(req.Content, 20)},
|
||||
}
|
||||
body := g.Map{
|
||||
"touser": req.Openid,
|
||||
"template_id": tplId,
|
||||
"page": "pkg-note/list",
|
||||
"data": data,
|
||||
}
|
||||
resp, err := g.Client().Post(ctx, xpayAPIBase+noteSubscribeSendPath+"?access_token="+token, body)
|
||||
if err != nil {
|
||||
return gerror.Newf("调用 subscribe/send 失败: %v", err)
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
var out struct {
|
||||
ErrCode int `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
}
|
||||
if err = json.Unmarshal(resp.ReadAll(), &out); err != nil {
|
||||
return gerror.Newf("解析 subscribe/send 响应失败: %v", err)
|
||||
}
|
||||
if out.ErrCode != 0 {
|
||||
return gerror.Newf("订阅消息发送失败: %d %s", out.ErrCode, out.ErrMsg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clampRunes 按字符数截断(订阅消息 thing 类型上限 20 个字符)
|
||||
func clampRunes(s string, max int) string {
|
||||
rs := []rune(s)
|
||||
if len(rs) <= max {
|
||||
return s
|
||||
}
|
||||
return string(rs[:max])
|
||||
}
|
||||
Reference in New Issue
Block a user