114 lines
3.3 KiB
Go
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
|
|
}
|