Files
yunzerwebsiteallinone/go/controllers/platform_wechat_mp.go
T
2026-09-16 18:15:58 +08:00

606 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
var follower *models.WechatMpFollower
if strings.TrimSpace(p.Scene) == "" {
// 未展示二维码(在公众号内发送「验证码」获取):按验证码认领待认领会话
follower, err = wechatmp.ConfirmVerifyCodeByCode(models.WechatBindTypePlatformUser, uint64(claims.UserID), 0, p.Code)
} else {
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
}