增加微信通知功能

This commit is contained in:
2026-09-16 18:06:18 +08:00
parent b48688cbab
commit f386899625
23 changed files with 3539 additions and 18 deletions
+124
View File
@@ -0,0 +1,124 @@
package controllers
import (
"io"
"strconv"
"strings"
"time"
"server/services/wechatmp"
beego "github.com/beego/beego/v2/server/web"
)
// WechatMpCallbackController 微信公众号服务器回调(微信服务器调用,无需平台鉴权)
//
// 公众号后台「设置与开发 → 服务器配置」填写:
// URL(服务器地址):https://<你的域名>/api/wechat/mp/callback
// Token:与平台「通知设置 → 微信配置」中的 Token 保持一致
// 消息加解密方式:明文 / 兼容 / 安全(三种均支持)
type WechatMpCallbackController struct {
beego.Controller
}
// Callback 服务器地址验证(GET,回显 echostr)与消息/事件推送(POST,被动回复)
func (c *WechatMpCallbackController) Callback() {
cfg, err := wechatmp.LoadEnabledConfig()
if err != nil {
c.Ctx.Output.SetStatus(403)
c.Ctx.Output.Body([]byte("wechat mp not configured"))
return
}
q := c.Ctx.Request.URL.Query()
timestamp := q.Get("timestamp")
nonce := q.Get("nonce")
// ---------------- GET:服务器地址校验 ----------------
if strings.EqualFold(c.Ctx.Input.Method(), "GET") {
echostr := q.Get("echostr")
if cfg.EncryptMode == wechatmp.EncryptModeSafe {
if !wechatmp.CheckMsgSignature(cfg.Token, timestamp, nonce, echostr, q.Get("msg_signature")) {
c.Ctx.Output.SetStatus(403)
c.Ctx.Output.Body([]byte("invalid signature"))
return
}
plain, derr := wechatmp.DecryptMessage(cfg.AESKey, cfg.AppID, echostr)
if derr != nil {
c.Ctx.Output.SetStatus(403)
c.Ctx.Output.Body([]byte("decrypt failed"))
return
}
c.Ctx.Output.Body([]byte(plain))
return
}
if !wechatmp.CheckSignature(cfg.Token, timestamp, nonce, q.Get("signature")) {
c.Ctx.Output.SetStatus(403)
c.Ctx.Output.Body([]byte("invalid signature"))
return
}
c.Ctx.Output.Body([]byte(echostr))
return
}
// ---------------- POST:消息 / 事件 ----------------
body, _ := io.ReadAll(c.Ctx.Request.Body)
rawBody := string(body)
if strings.TrimSpace(rawBody) == "" {
c.Ctx.Output.Body([]byte(""))
return
}
needEncryptReply := false
if strings.Contains(rawBody, "<Encrypt>") {
// 安全/兼容模式:外层为加密 XML,先验签再解密
outer, perr := wechatmp.ParseInboundXML(rawBody)
if perr != nil {
c.Ctx.Output.Body([]byte(""))
return
}
if !wechatmp.CheckMsgSignature(cfg.Token, timestamp, nonce, outer.Encrypt, q.Get("msg_signature")) {
c.Ctx.Output.Body([]byte(""))
return
}
plain, derr := wechatmp.DecryptMessage(cfg.AESKey, cfg.AppID, outer.Encrypt)
if derr != nil {
c.Ctx.Output.Body([]byte(""))
return
}
rawBody = plain
needEncryptReply = true
} else if !wechatmp.CheckSignature(cfg.Token, timestamp, nonce, q.Get("signature")) {
// 明文模式:签名校验不通过直接拒绝(微信会重试)
c.Ctx.Output.Body([]byte(""))
return
}
msg, merr := wechatmp.ParseInboundXML(rawBody)
if merr != nil {
c.Ctx.Output.Body([]byte(""))
return
}
reply := wechatmp.HandleInbound(cfg, msg)
if strings.TrimSpace(reply) == "" {
// 无需回复:返回空串,微信视为处理成功
c.Ctx.Output.Body([]byte(""))
return
}
now := time.Now().Unix()
plainReply := wechatmp.BuildTextReply(msg.FromUserName, msg.ToUserName, reply, now)
c.Ctx.Output.Header("Content-Type", "application/xml; charset=utf-8")
if needEncryptReply {
ts := strconv.FormatInt(now, 10)
enc, eerr := wechatmp.BuildEncryptedReply(cfg, plainReply, ts, nonce)
if eerr != nil {
c.Ctx.Output.Body([]byte(""))
return
}
c.Ctx.Output.Body([]byte(enc))
return
}
c.Ctx.Output.Body([]byte(plainReply))
}
+202
View File
@@ -0,0 +1,202 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
"server/services/wechatmp"
beego "github.com/beego/beego/v2/server/web"
)
// =============================================================
// 租户端「微信公众号绑定」:
// 扫码关注公众号 → 公众号被动回复验证码 → 本端输入验证码完成绑定
// 绑定后(公众号认证后)可接收平台/租户侧的提醒、公告推送
// =============================================================
// BackendWechatMpController 租户端微信公众号绑定
type BackendWechatMpController struct {
beego.Controller
}
func (c *BackendWechatMpController) claims() (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
return nil, fmt.Errorf("未登录")
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, fmt.Errorf("认证信息格式错误")
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil {
return nil, fmt.Errorf("无效的token")
}
if claims.UserType != "backend" && claims.UserType != "app" {
return nil, fmt.Errorf("无权访问")
}
return claims, nil
}
func (c *BackendWechatMpController) jsonErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
func (c *BackendWechatMpController) ok(data interface{}, msg string) {
if msg == "" {
msg = "success"
}
resp := map[string]interface{}{"code": 200, "msg": msg}
if data != nil {
resp["data"] = data
}
c.Data["json"] = resp
_ = c.ServeJSON()
}
func (c *BackendWechatMpController) readBody(out interface{}) bool {
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.jsonErr(400, 400, "参数错误")
return false
}
if err := json.Unmarshal(raw, out); err != nil {
c.jsonErr(400, 400, "参数错误")
return false
}
return true
}
// BindStart POST /backend/wechatMp/bind/start
func (c *BackendWechatMpController) BindStart() {
claims, err := c.claims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
uid := uint64(claims.UserID)
tid := uint64(claims.TenantId)
if uid == 0 || tid == 0 {
c.jsonErr(401, 401, "未获取到用户信息")
return
}
scene, _, qrURL, expireAt, err := wechatmp.CreateVerifyCode(models.WechatBindTypeTenantUser, uid, tid, 0)
if err != nil {
c.jsonErr(400, 400, "生成二维码失败:"+err.Error())
return
}
c.ok(map[string]interface{}{
"scene": scene,
"qrcode_url": qrURL,
"expire_at": expireAt,
"expires_in": int(time.Until(expireAt).Seconds()),
}, "生成成功")
}
// BindStatus GET /backend/wechatMp/bind/status?scene=xxx
func (c *BackendWechatMpController) BindStatus() {
claims, err := c.claims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
scene := strings.TrimSpace(c.GetString("scene"))
row, err := wechatmp.GetVerifyCode(scene)
if err != nil {
c.jsonErr(404, 404, err.Error())
return
}
if row.BindType != models.WechatBindTypeTenantUser || row.BindID != uint64(claims.UserID) {
c.jsonErr(403, 403, "该二维码不是当前账号发起,请重新获取")
return
}
c.ok(map[string]interface{}{
"status": row.Status,
"expire_at": row.ExpireAt,
}, "获取成功")
}
// BindConfirm POST /backend/wechatMp/bind/confirm {scene, code}
func (c *BackendWechatMpController) BindConfirm() {
claims, err := c.claims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
var p struct {
Scene string `json:"scene"`
Code string `json:"code"`
}
if !c.readBody(&p) {
return
}
if strings.TrimSpace(p.Code) == "" {
c.jsonErr(400, 400, "请输入公众号返回的验证码")
return
}
follower, err := wechatmp.ConfirmVerifyCode(
models.WechatBindTypeTenantUser,
uint64(claims.UserID),
uint64(claims.TenantId),
p.Scene,
p.Code,
)
if err != nil {
c.jsonErr(400, 400, err.Error())
return
}
c.ok(map[string]interface{}{
"openid": follower.OpenID,
"nickname": follower.Nickname,
"bind_time": follower.BindTime,
}, "绑定成功")
}
// MyBinding GET /backend/wechatMp/my
func (c *BackendWechatMpController) MyBinding() {
claims, err := c.claims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
follower, err := wechatmp.GetFollowerByUser(models.WechatBindTypeTenantUser, uint64(claims.UserID))
if err != nil {
c.jsonErr(500, 500, "查询失败:"+err.Error())
return
}
if follower == nil || follower.BindTid != uint64(claims.TenantId) {
c.ok(map[string]interface{}{"bound": false}, "获取成功")
return
}
c.ok(map[string]interface{}{
"bound": true,
"openid": follower.OpenID,
"nickname": follower.Nickname,
"avatar": follower.Avatar,
"subscribe": follower.Subscribe,
"bind_time": follower.BindTime,
"subscribe_time": follower.SubscribeTime,
}, "获取成功")
}
// Unbind POST /backend/wechatMp/unbind
func (c *BackendWechatMpController) Unbind() {
claims, err := c.claims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
if err := wechatmp.UnbindByUser(models.WechatBindTypeTenantUser, uint64(claims.UserID)); err != nil {
c.jsonErr(500, 500, "解绑失败:"+err.Error())
return
}
c.ok(nil, "已解绑")
}
+599
View File
@@ -0,0 +1,599 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strings"
"time"
"server/models"
"server/pkg/jwtutil"
"server/services/wechatmp"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
// =============================================================
// 平台端「微信公众号(服务号)」:
// 配置(AppID/Secret/Token/AESKey)、绑定(扫码关注取验证码)、粉丝管理、消息推送
// 未认证阶段可完整跑通「扫码关注 → 公众号回复验证码 → 平台核销绑定」;
// 认证后开启模板消息推送(提醒/公告)。
// =============================================================
// PlatformWechatMpController 平台端微信公众号管理
type PlatformWechatMpController struct {
beego.Controller
}
func (c *PlatformWechatMpController) claims() (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
return nil, fmt.Errorf("未登录")
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, fmt.Errorf("认证信息格式错误")
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil {
return nil, fmt.Errorf("无效的token")
}
if claims.UserType != "platform" {
return nil, fmt.Errorf("无权访问")
}
return claims, nil
}
func (c *PlatformWechatMpController) jsonErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
func (c *PlatformWechatMpController) ok(data interface{}, msg string) {
if msg == "" {
msg = "success"
}
resp := map[string]interface{}{"code": 200, "msg": msg}
if data != nil {
resp["data"] = data
}
c.Data["json"] = resp
_ = c.ServeJSON()
}
func (c *PlatformWechatMpController) readBody(out interface{}) bool {
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil {
c.jsonErr(400, 400, "参数错误")
return false
}
if err := json.Unmarshal(raw, out); err != nil {
c.jsonErr(400, 400, "参数错误")
return false
}
return true
}
// ============================ 配置 ============================
// GetConfig GET /platform/wechatMp/config
func (c *PlatformWechatMpController) GetConfig() {
if _, err := c.claims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
data := map[string]interface{}{
"app_id": "",
"app_secret": "",
"token": "",
"aes_key": "",
"encrypt_mode": wechatmp.EncryptModePlain,
"template_id": "",
"verified": 0,
"enabled": 0,
"remark": "",
"configured": false,
"callback_url": wechatmp.CallbackURL(),
}
if cfg, err := wechatmp.LoadConfig(); err == nil {
data["app_id"] = cfg.AppID
data["app_secret"] = wechatmp.MaskSecret(cfg.AppSecret)
data["token"] = cfg.Token
data["aes_key"] = wechatmp.MaskSecret(cfg.AESKey)
data["encrypt_mode"] = cfg.EncryptMode
data["template_id"] = cfg.TemplateID
data["remark"] = cfg.Remark
data["configured"] = cfg.AppID != ""
if cfg.Verified {
data["verified"] = 1
}
if cfg.Enabled {
data["enabled"] = 1
}
}
c.ok(data, "获取成功")
}
// SaveConfig POST /platform/wechatMp/config
func (c *PlatformWechatMpController) SaveConfig() {
if _, err := c.claims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
var p struct {
AppID string `json:"app_id"`
AppSecret string `json:"app_secret"`
Token string `json:"token"`
AESKey string `json:"aes_key"`
EncryptMode string `json:"encrypt_mode"`
TemplateID string `json:"template_id"`
Verified bool `json:"verified"`
Enabled bool `json:"enabled"`
Remark string `json:"remark"`
}
if !c.readBody(&p) {
return
}
if strings.TrimSpace(p.AppID) == "" {
c.jsonErr(400, 400, "AppID 不能为空")
return
}
err := wechatmp.SaveConfig(wechatmp.SaveInput{
AppID: p.AppID,
AppSecret: p.AppSecret,
Token: p.Token,
AESKey: p.AESKey,
EncryptMode: p.EncryptMode,
TemplateID: p.TemplateID,
Verified: p.Verified,
Enabled: p.Enabled,
Remark: p.Remark,
})
if err != nil {
c.jsonErr(400, 400, "保存失败:"+err.Error())
return
}
c.ok(nil, "保存成功")
}
// TestConnection POST /platform/wechatMp/test
// 使用当前配置获取 access_token,验证 AppID/AppSecret 是否正确
func (c *PlatformWechatMpController) TestConnection() {
if _, err := c.claims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
cfg, err := wechatmp.LoadConfig()
if err != nil || cfg.AppID == "" || cfg.AppSecret == "" {
c.jsonErr(400, 400, "请先保存 AppID 与 AppSecret")
return
}
token, err := wechatmp.GetAccessToken(cfg)
if err != nil {
msg := "连接失败:" + err.Error()
if hint, ok := wechatmp.ExplainError(err); ok {
msg += ";" + hint
}
c.jsonErr(400, 400, msg)
return
}
c.ok(map[string]interface{}{
"access_token_suffix": wechatmp.MaskSecret(token),
"callback_url": wechatmp.CallbackURL(),
"verified": boolToInt8(cfg.Verified),
}, "连接成功:AppID 与 AppSecret 校验通过")
}
// ============================ 绑定(当前平台用户) ============================
// BindStart POST /platform/wechatMp/bind/start
// 生成带参二维码;用户扫码关注后公众号将被动回复验证码
func (c *PlatformWechatMpController) BindStart() {
claims, err := c.claims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
uid := uint64(claims.UserID)
if uid == 0 {
c.jsonErr(401, 401, "未获取到用户信息")
return
}
scene, _, qrURL, expireAt, err := wechatmp.CreateVerifyCode(models.WechatBindTypePlatformUser, uid, 0, 0)
if err != nil {
c.jsonErr(400, 400, "生成二维码失败:"+err.Error())
return
}
c.ok(map[string]interface{}{
"scene": scene,
"qrcode_url": qrURL,
"expire_at": expireAt,
"expires_in": int(time.Until(expireAt).Seconds()),
"callback_url": wechatmp.CallbackURL(),
}, "生成成功")
}
// BindStatus GET /platform/wechatMp/bind/status?scene=xxx
func (c *PlatformWechatMpController) BindStatus() {
claims, err := c.claims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
scene := strings.TrimSpace(c.GetString("scene"))
row, err := wechatmp.GetVerifyCode(scene)
if err != nil {
c.jsonErr(404, 404, err.Error())
return
}
if row.BindType != models.WechatBindTypePlatformUser || row.BindID != uint64(claims.UserID) {
c.jsonErr(403, 403, "该二维码不是当前账号发起,请重新获取")
return
}
c.ok(map[string]interface{}{
"status": row.Status,
"expire_at": row.ExpireAt,
}, "获取成功")
}
// BindConfirm POST /platform/wechatMp/bind/confirm {scene, code}
func (c *PlatformWechatMpController) BindConfirm() {
claims, err := c.claims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
var p struct {
Scene string `json:"scene"`
Code string `json:"code"`
}
if !c.readBody(&p) {
return
}
if strings.TrimSpace(p.Code) == "" {
c.jsonErr(400, 400, "请输入公众号返回的验证码")
return
}
follower, err := wechatmp.ConfirmVerifyCode(models.WechatBindTypePlatformUser, uint64(claims.UserID), 0, p.Scene, p.Code)
if err != nil {
c.jsonErr(400, 400, err.Error())
return
}
c.ok(map[string]interface{}{
"openid": follower.OpenID,
"nickname": follower.Nickname,
"bind_time": follower.BindTime,
}, "绑定成功")
}
// MyBinding GET /platform/wechatMp/my
func (c *PlatformWechatMpController) MyBinding() {
claims, err := c.claims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
follower, err := wechatmp.GetFollowerByUser(models.WechatBindTypePlatformUser, uint64(claims.UserID))
if err != nil {
c.jsonErr(500, 500, "查询失败:"+err.Error())
return
}
if follower == nil {
c.ok(map[string]interface{}{"bound": false}, "获取成功")
return
}
c.ok(map[string]interface{}{
"bound": true,
"openid": follower.OpenID,
"nickname": follower.Nickname,
"avatar": follower.Avatar,
"subscribe": follower.Subscribe,
"bind_time": follower.BindTime,
"subscribe_time": follower.SubscribeTime,
}, "获取成功")
}
// Unbind POST /platform/wechatMp/unbind
func (c *PlatformWechatMpController) Unbind() {
claims, err := c.claims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
if err := wechatmp.UnbindByUser(models.WechatBindTypePlatformUser, uint64(claims.UserID)); err != nil {
c.jsonErr(500, 500, "解绑失败:"+err.Error())
return
}
c.ok(nil, "已解绑")
}
// ============================ 粉丝管理 ============================
// ListFollowers GET /platform/wechatMp/followers?page=&pageSize=&keyword=&bind_type=&subscribe=
func (c *PlatformWechatMpController) ListFollowers() {
if _, err := c.claims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 20)
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 200 {
pageSize = 20
}
qs := models.Orm.QueryTable(new(models.WechatMpFollower)).Filter("delete_time__isnull", true)
if kw := strings.TrimSpace(c.GetString("keyword")); kw != "" {
cond := orm.NewCondition().
Or("nickname__icontains", kw).
Or("openid__icontains", kw).
Or("remark__icontains", kw)
qs = qs.SetCond(cond)
}
if bindType := strings.TrimSpace(c.GetString("bind_type")); bindType != "" {
qs = qs.Filter("bind_type", bindType)
}
if sub := strings.TrimSpace(c.GetString("subscribe")); sub == "0" || sub == "1" {
qs = qs.Filter("subscribe", sub)
}
total, err := qs.Count()
if err != nil {
c.jsonErr(500, 500, "获取失败:"+err.Error())
return
}
var rows []models.WechatMpFollower
if _, err := qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows); err != nil {
c.jsonErr(500, 500, "获取失败:"+err.Error())
return
}
// 绑定对象名称(平台用户 / 租户用户)
nameMap := loadWechatBindNames(rows)
list := make([]map[string]interface{}, 0, len(rows))
for i := range rows {
r := rows[i]
nickname := r.Nickname
if strings.TrimSpace(nickname) == "" {
nickname = maskOpenID(r.OpenID)
}
list = append(list, map[string]interface{}{
"id": r.ID,
"openid": r.OpenID,
"nickname": nickname,
"avatar": r.Avatar,
"sex": r.Sex,
"city": r.City,
"subscribe": r.Subscribe,
"subscribe_time": r.SubscribeTime,
"unsubscribe_time": r.UnsubscribeTime,
"bind_type": r.BindType,
"bind_id": r.BindID,
"bind_tid": r.BindTid,
"bind_name": nameMap[r.BindType+"#"+fmt.Sprintf("%d", r.BindID)+"#"+fmt.Sprintf("%d", r.BindTid)],
"bind_time": r.BindTime,
"remark": r.Remark,
"create_time": r.CreateTime,
})
}
c.ok(map[string]interface{}{"list": list, "total": total, "page": page, "pageSize": pageSize}, "获取成功")
}
// SetFollowerRemark POST /platform/wechatMp/follower/remark {id, remark}
func (c *PlatformWechatMpController) SetFollowerRemark() {
if _, err := c.claims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
var p struct {
ID uint64 `json:"id"`
Remark string `json:"remark"`
}
if !c.readBody(&p) {
return
}
if p.ID == 0 {
c.jsonErr(400, 400, "参数错误")
return
}
now := time.Now()
if _, err := models.Orm.QueryTable(new(models.WechatMpFollower)).
Filter("id", p.ID).
Update(map[string]interface{}{"remark": strings.TrimSpace(p.Remark), "update_time": now}); err != nil {
c.jsonErr(500, 500, "保存失败:"+err.Error())
return
}
c.ok(nil, "保存成功")
}
// UnbindFollower POST /platform/wechatMp/follower/unbind {id}
// 仅清除绑定关系(保留粉丝记录)
func (c *PlatformWechatMpController) UnbindFollower() {
if _, err := c.claims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
var p struct {
ID uint64 `json:"id"`
}
if !c.readBody(&p) {
return
}
if p.ID == 0 {
c.jsonErr(400, 400, "参数错误")
return
}
now := time.Now()
if _, err := models.Orm.QueryTable(new(models.WechatMpFollower)).
Filter("id", p.ID).
Update(map[string]interface{}{
"bind_type": "", "bind_id": 0, "bind_tid": 0, "update_time": now,
}); err != nil {
c.jsonErr(500, 500, "解绑失败:"+err.Error())
return
}
c.ok(nil, "已解绑")
}
// ============================ 推送 ============================
// Push POST /platform/wechatMp/push
// {title, content, remark, url, bind_type, bind_id, tid}
func (c *PlatformWechatMpController) Push() {
if _, err := c.claims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
var p struct {
Title string `json:"title"`
Content string `json:"content"`
Remark string `json:"remark"`
URL string `json:"url"`
BindType string `json:"bind_type"`
BindID uint64 `json:"bind_id"`
Tid uint64 `json:"tid"`
}
if !c.readBody(&p) {
return
}
if strings.TrimSpace(p.Title) == "" && strings.TrimSpace(p.Content) == "" {
c.jsonErr(400, 400, "请填写推送内容")
return
}
cfg, err := wechatmp.LoadEnabledConfig()
if err != nil {
c.jsonErr(400, 400, "请先启用并配置微信公众号:"+err.Error())
return
}
sent, failed, perr := wechatmp.PushToFollowers(cfg, wechatmp.PushScope{
BindType: strings.TrimSpace(p.BindType),
BindID: p.BindID,
Tid: p.Tid,
}, wechatmp.PushMessage{
Title: strings.TrimSpace(p.Title),
Content: strings.TrimSpace(p.Content),
Remark: strings.TrimSpace(p.Remark),
URL: strings.TrimSpace(p.URL),
})
if perr != nil {
c.jsonErr(400, 400, perr.Error())
return
}
c.ok(map[string]interface{}{"sent": sent, "failed": failed},
fmt.Sprintf("推送完成:成功 %d 条,失败 %d 条", sent, failed))
}
// PushTest POST /platform/wechatMp/push/test {title, content, remark}
// 推送给当前登录平台用户绑定的微信
func (c *PlatformWechatMpController) PushTest() {
claims, err := c.claims()
if err != nil {
c.jsonErr(401, 401, err.Error())
return
}
var p struct {
Title string `json:"title"`
Content string `json:"content"`
Remark string `json:"remark"`
}
if !c.readBody(&p) {
return
}
cfg, err := wechatmp.LoadEnabledConfig()
if err != nil {
c.jsonErr(400, 400, "请先启用并配置微信公众号:"+err.Error())
return
}
follower, err := wechatmp.GetFollowerByUser(models.WechatBindTypePlatformUser, uint64(claims.UserID))
if err != nil || follower == nil {
c.jsonErr(400, 400, "当前账号尚未绑定微信,请先扫码绑定")
return
}
if follower.Subscribe != 1 {
c.jsonErr(400, 400, "该微信已取消关注,请重新关注后再试")
return
}
if p.Title == "" {
p.Title = "平台消息测试"
}
if p.Content == "" {
p.Content = "这是一条来自平台的测试消息,收到说明推送链路正常。"
}
if err := wechatmp.PushToOpenID(cfg, follower.OpenID, wechatmp.PushMessage{
Title: strings.TrimSpace(p.Title),
Content: strings.TrimSpace(p.Content),
Remark: strings.TrimSpace(p.Remark),
}); err != nil {
c.jsonErr(400, 400, "推送失败:"+err.Error())
return
}
c.ok(nil, "测试消息已发送")
}
// ============================ 辅助 ============================
func boolToInt8(b bool) int8 {
if b {
return 1
}
return 0
}
func maskOpenID(openid string) string {
if len(openid) <= 8 {
return openid
}
return openid[:4] + "****" + openid[len(openid)-4:]
}
// loadWechatBindNames 批量加载粉丝绑定对象名称:key = bindType#bindID#bindTid
func loadWechatBindNames(rows []models.WechatMpFollower) map[string]string {
names := map[string]string{}
if len(rows) == 0 {
return names
}
adminIDs := make([]uint64, 0)
tenantIDs := make([]uint64, 0)
for _, r := range rows {
switch r.BindType {
case models.WechatBindTypePlatformUser:
adminIDs = append(adminIDs, r.BindID)
case models.WechatBindTypeTenantUser:
tenantIDs = append(tenantIDs, r.BindID)
}
}
if len(adminIDs) > 0 {
var admins []models.AdminUser
if _, err := models.Orm.QueryTable(new(models.AdminUser)).Filter("id__in", adminIDs).All(&admins, "ID", "Account", "Name"); err == nil {
for _, a := range admins {
name := a.Account
if a.Name != nil && strings.TrimSpace(*a.Name) != "" {
name = *a.Name
}
names[models.WechatBindTypePlatformUser+"#"+fmt.Sprintf("%d", a.ID)+"#0"] = name
}
}
}
if len(tenantIDs) > 0 {
var users []models.SystemTenantUser
if _, err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("uid__in", tenantIDs).All(&users, "UID", "Tid", "Account", "Name"); err == nil {
for _, u := range users {
name := ""
if u.Account != nil {
name = *u.Account
}
if u.Name != nil && strings.TrimSpace(*u.Name) != "" {
name = *u.Name
}
names[models.WechatBindTypeTenantUser+"#"+fmt.Sprintf("%d", u.Uid)+"#"+fmt.Sprintf("%d", u.Tid)] = name
}
}
}
return names
}