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 }