增加微信通知功能
This commit is contained in:
@@ -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))
|
||||
}
|
||||
@@ -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, "已解绑")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user