Files
2026-09-16 18:06:18 +08:00

114 lines
3.3 KiB
Go

package wechatmp
import (
"errors"
"time"
"server/models"
)
// =============================================================
// 消息推送(认证后可用):
// 未认证服务号微信不允许主动推送(模板消息/订阅通知/客服消息均受限),
// 本层只负责在认证后通过模板消息把提醒/公告发给已关注粉丝;
// 未认证时返回 ErrNotVerified,由上层给出明确提示。
// =============================================================
// ErrNotVerified 公众号未认证,无法主动推送
var ErrNotVerified = errors.New("公众号未认证:模板消息推送需认证后使用(当前仅支持关注与被动回复)")
// PushMessage 推送内容(对应「标题/内容/备注」通用模板字段)
type PushMessage struct {
Title string // first:首行说明
Content string // keyword1:主体内容
Remark string // remark:备注
URL string // 点击跳转地址(可选)
}
// PushToOpenID 向指定粉丝推送一条模板消息
func PushToOpenID(cfg *Config, openid string, msg PushMessage) error {
if cfg == nil {
return ErrNotConfigured
}
if !cfg.Verified {
return ErrNotVerified
}
if openid == "" {
return errors.New("openid 为空")
}
if msg.Title == "" {
msg.Title = "您有一条新的消息"
}
if len([]rune(msg.Title)) > 100 {
msg.Title = string([]rune(msg.Title)[:100])
}
if len([]rune(msg.Content)) > 200 {
msg.Content = string([]rune(msg.Content)[:200])
}
if len([]rune(msg.Remark)) > 100 {
msg.Remark = string([]rune(msg.Remark)[:100])
}
return SendTemplateMessage(cfg, openid, msg.URL, map[string]string{
"first": msg.Title,
"keyword1": msg.Content,
"remark": msg.Remark,
})
}
// PushScope 推送范围
type PushScope struct {
// BindType 为空表示全部已关注粉丝;platform_user / tenant_user 只推绑定对应类型账号的粉丝
BindType string
// BindID 非 0 时只推给该绑定账号(与 BindType 组合使用)
BindID uint64
// Tid 非 0 时只推给绑定该租户的用户(tenant_user 场景)
Tid uint64
}
// MaxBatchPush 单次批量推送上限
const MaxBatchPush = 500
// PushToFollowers 按范围批量推送,返回成功/失败数量。
// 仅推送给「已关注(subscribe=1)且未删除」的粉丝。
func PushToFollowers(cfg *Config, scope PushScope, msg PushMessage) (sent, failed int, err error) {
if cfg == nil {
return 0, 0, ErrNotConfigured
}
if !cfg.Verified {
return 0, 0, ErrNotVerified
}
qs := models.Orm.QueryTable(new(models.WechatMpFollower)).
Filter("subscribe", 1).
Filter("delete_time__isnull", true)
if scope.BindType != "" {
qs = qs.Filter("bind_type", scope.BindType)
}
if scope.BindID > 0 {
qs = qs.Filter("bind_id", scope.BindID)
}
if scope.Tid > 0 {
qs = qs.Filter("bind_tid", scope.Tid)
}
var rows []models.WechatMpFollower
if _, qerr := qs.OrderBy("-id").Limit(MaxBatchPush).All(&rows); qerr != nil {
return 0, 0, qerr
}
if len(rows) == 0 {
return 0, 0, errors.New("没有匹配的已关注粉丝")
}
for i := range rows {
if perr := PushToOpenID(cfg, rows[i].OpenID, msg); perr != nil {
failed++
continue
}
sent++
// 简单节流,避免触发微信频率限制
if i%20 == 19 {
time.Sleep(200 * time.Millisecond)
}
}
return sent, failed, nil
}