优化小程序部分
This commit is contained in:
1 parent
727319a6eb
commit
d4e89788c5
46 files changed
+4906
-63
No files matched your search
@@ -0,0 +1,196 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"server/pkg/identity"
|
||||
"server/pkg/jwtutil"
|
||||
"server/services/wechatmini"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// AppMiniSubscribeController 小程序订阅消息(uniapp 调用)
|
||||
//
|
||||
// 订阅消息是「用户 → 授权 → 服务器主动推给用户」的链路:
|
||||
// 1. GET /api/mini/subscribe/templates 取当前可订阅的模板(含模板ID与跳转页)
|
||||
// 2. 小程序端调 uni.requestSubscribeMessage 弹授权窗,用户同意后
|
||||
// 3. POST /api/mini/subscribe/authorize 上报授权结果,服务器记录一次发送机会
|
||||
// 4. 之后业务侧发站内信/审批待办时,服务器自动用掉这次机会推送
|
||||
type AppMiniSubscribeController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *AppMiniSubscribeController) appClaims() (*jwtutil.Claims, error) {
|
||||
return identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndApp, identity.EndBackend)
|
||||
}
|
||||
|
||||
func (c *AppMiniSubscribeController) 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 *AppMiniSubscribeController) ok(data interface{}, msg string) {
|
||||
if msg == "" {
|
||||
msg = "success"
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": msg, "data": data}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Templates GET /api/mini/subscribe/templates
|
||||
// 返回当前启用的订阅模板:小程序端拿 template_id 去调 requestSubscribeMessage
|
||||
func (c *AppMiniSubscribeController) Templates() {
|
||||
claims, err := c.appClaims()
|
||||
if err != nil {
|
||||
code := identity.Status(err)
|
||||
c.jsonErr(code, code, err.Error())
|
||||
return
|
||||
}
|
||||
identityID := uint64(claims.UserID)
|
||||
|
||||
apps, err := wechatmini.ListApps()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "读取小程序配置失败")
|
||||
return
|
||||
}
|
||||
list := make([]map[string]interface{}, 0)
|
||||
for _, app := range apps {
|
||||
if app.Enabled != 1 {
|
||||
continue
|
||||
}
|
||||
tpls, terr := wechatmini.ListTemplates(app.AppID)
|
||||
if terr != nil {
|
||||
continue
|
||||
}
|
||||
for _, tpl := range tpls {
|
||||
if tpl.Enabled != 1 || tpl.TemplateID == "" {
|
||||
continue
|
||||
}
|
||||
list = append(list, map[string]interface{}{
|
||||
"app_id": app.AppID,
|
||||
"code": tpl.Code,
|
||||
"title": tpl.Title,
|
||||
"template_id": tpl.TemplateID,
|
||||
"page": tpl.Page,
|
||||
"remain": wechatmini.SubscribeRemain(identityID, app.AppID, tpl.TemplateID),
|
||||
})
|
||||
}
|
||||
}
|
||||
c.ok(map[string]interface{}{"list": list}, "获取成功")
|
||||
}
|
||||
|
||||
// Authorize POST /api/mini/subscribe/authorize
|
||||
// 上报用户授权结果:每授权一次,服务器记录一次可发送机会
|
||||
func (c *AppMiniSubscribeController) Authorize() {
|
||||
claims, err := c.appClaims()
|
||||
if err != nil {
|
||||
code := identity.Status(err)
|
||||
c.jsonErr(code, code, err.Error())
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
TemplateIDs []string `json:"template_ids"`
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if len(p.TemplateIDs) == 0 {
|
||||
c.jsonErr(400, 400, "缺少 template_ids")
|
||||
return
|
||||
}
|
||||
identityID := uint64(claims.UserID)
|
||||
apps, err := wechatmini.ListApps()
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "读取小程序配置失败")
|
||||
return
|
||||
}
|
||||
granted := 0
|
||||
for _, app := range apps {
|
||||
if app.Enabled != 1 {
|
||||
continue
|
||||
}
|
||||
// 只记录平台确实配置过的模板
|
||||
for _, tid := range p.TemplateIDs {
|
||||
tid = strings.TrimSpace(tid)
|
||||
if tid == "" {
|
||||
continue
|
||||
}
|
||||
tpls, terr := wechatmini.ListTemplates(app.AppID)
|
||||
if terr != nil {
|
||||
continue
|
||||
}
|
||||
matched := false
|
||||
for _, tpl := range tpls {
|
||||
if tpl.TemplateID == tid {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
if gerr := wechatmini.GrantSubscribe(identityID, app.AppID, []string{tid}); gerr == nil {
|
||||
granted++
|
||||
}
|
||||
}
|
||||
}
|
||||
c.ok(map[string]interface{}{"granted": granted}, "已记录授权")
|
||||
}
|
||||
|
||||
// TestNotice POST /api/mini/subscribe/test
|
||||
// 给自己发一条测试订阅消息(用于验证模板配置是否正确)
|
||||
func (c *AppMiniSubscribeController) TestNotice() {
|
||||
claims, err := c.appClaims()
|
||||
if err != nil {
|
||||
code := identity.Status(err)
|
||||
c.jsonErr(code, code, err.Error())
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
_ = json.Unmarshal(raw, &p)
|
||||
title := strings.TrimSpace(p.Title)
|
||||
if title == "" {
|
||||
title = "测试提醒"
|
||||
}
|
||||
content := strings.TrimSpace(p.Content)
|
||||
if content == "" {
|
||||
content = "这是一条订阅消息测试,收到说明配置成功。"
|
||||
}
|
||||
|
||||
identityID := uint64(claims.UserID)
|
||||
if wechatmini.MiniOpenIDByIdentity(identityID) == "" {
|
||||
c.jsonErr(400, 400, "当前账号未绑定小程序,请先用小程序登录")
|
||||
return
|
||||
}
|
||||
res := wechatmini.PushSubscribe(wechatmini.SceneNotice, identityID, map[string]string{
|
||||
"title": title,
|
||||
"content": content,
|
||||
})
|
||||
if res.Sent > 0 {
|
||||
c.ok(map[string]interface{}{"sent": res.Sent}, "已发送")
|
||||
return
|
||||
}
|
||||
if len(res.Errors) > 0 {
|
||||
msg := res.Errors[0].Error()
|
||||
if hint := wechatmini.ExplainSubscribeError(res.Errors[0]); hint != "" {
|
||||
msg = hint
|
||||
}
|
||||
c.jsonErr(400, 400, msg)
|
||||
return
|
||||
}
|
||||
c.jsonErr(400, 400, "没有可用的订阅:模板未配置,或你还没授权订阅消息")
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/services/wechatmini"
|
||||
"server/services/wechatmp"
|
||||
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// WechatMiniCallbackController 微信小程序消息推送回调(微信服务器调用,无需平台鉴权)
|
||||
//
|
||||
// 小程序后台「开发管理 → 开发设置 → 消息推送」填写:
|
||||
//
|
||||
// URL(服务器地址):https://<你的域名>/api/wechat/mini/callback
|
||||
// Token:与平台端「小程序配置 - 消息推送」中的 Token 一致
|
||||
// EncodingAESKey:与平台端配置一致(安全 / 兼容模式必填)
|
||||
// 消息加解密方式:明文 / 兼容 / 安全(三种均支持)
|
||||
// 数据格式:XML / JSON(均支持,按平台端配置输出)
|
||||
//
|
||||
// 平台端维护入口:系统设置 → 小程序配置 → 编辑小程序 → 消息推送
|
||||
type WechatMiniCallbackController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// Callback 服务器地址验证(GET,回显 echostr)与消息 / 事件推送(POST,被动回复)
|
||||
func (c *WechatMiniCallbackController) Callback() {
|
||||
cfgs, err := wechatmini.LoadPushConfigs()
|
||||
if err != nil || len(cfgs) == 0 {
|
||||
c.Ctx.Output.SetStatus(403)
|
||||
c.Ctx.Output.Body([]byte("wechat mini push 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 reply, ok := verifyPushEchostr(cfgs, timestamp, nonce, q.Get("signature"), q.Get("msg_signature"), echostr); ok {
|
||||
c.Ctx.Output.Body([]byte(reply))
|
||||
return
|
||||
}
|
||||
c.Ctx.Output.SetStatus(403)
|
||||
c.Ctx.Output.Body([]byte("invalid signature"))
|
||||
return
|
||||
}
|
||||
|
||||
// ---------------- POST:消息 / 事件 ----------------
|
||||
body, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
raw := strings.TrimSpace(string(body))
|
||||
if raw == "" {
|
||||
c.Ctx.Output.Body([]byte(""))
|
||||
return
|
||||
}
|
||||
|
||||
format := wechatmini.DetectPushFormat(raw)
|
||||
msg, perr := parsePushBody(raw, format)
|
||||
if perr != nil {
|
||||
logs.Info("[wechat-mini] 推送消息解析失败: %v", perr)
|
||||
c.Ctx.Output.Body([]byte(""))
|
||||
return
|
||||
}
|
||||
|
||||
var cfg *wechatmini.PushConfig
|
||||
encrypted := false
|
||||
|
||||
if msg.Encrypt != "" {
|
||||
// 安全模式:整个请求体是密文信封,验签后解密
|
||||
msgSig := q.Get("msg_signature")
|
||||
if msgSig == "" {
|
||||
msgSig = msg.MsgSignature
|
||||
}
|
||||
if c2, ok := wechatmini.MatchPushConfigByMsgSignature(cfgs, timestamp, nonce, msg.Encrypt, msgSig); ok &&
|
||||
c2.EncryptMode == wechatmp.EncryptModeSafe {
|
||||
plain, derr := wechatmini.DecryptPushMessage(c2, msg.Encrypt)
|
||||
if derr != nil {
|
||||
logs.Info("[wechat-mini] 推送消息解密失败: %v", derr)
|
||||
c.Ctx.Output.Body([]byte(""))
|
||||
return
|
||||
}
|
||||
inner, ierr := parsePushBody(plain, format)
|
||||
if ierr != nil {
|
||||
logs.Info("[wechat-mini] 解密后消息解析失败: %v", ierr)
|
||||
c.Ctx.Output.Body([]byte(""))
|
||||
return
|
||||
}
|
||||
cfg = c2
|
||||
msg = inner
|
||||
encrypted = true
|
||||
}
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
// 明文模式 / 兼容模式(兼容模式请求体本身即明文,仅额外携带 Encrypt 字段)
|
||||
if c3, ok := wechatmini.MatchPushConfigBySignature(cfgs, timestamp, nonce, q.Get("signature")); ok {
|
||||
cfg = c3
|
||||
}
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
logs.Info("[wechat-mini] 推送消息签名校验失败(timestamp=%s nonce=%s)", timestamp, nonce)
|
||||
c.Ctx.Output.Body([]byte(""))
|
||||
return
|
||||
}
|
||||
|
||||
// 排查用:记录微信推送的消息 / 事件类型
|
||||
logs.Info("[wechat-mini] 收到推送: appid=%s type=%s event=%s from=%s key=%s",
|
||||
cfg.AppID, msg.MsgType, msg.Event, msg.FromUserName, msg.EventKey)
|
||||
|
||||
reply := wechatmini.HandlePushMessage(cfg, msg)
|
||||
if reply == "" {
|
||||
// 无需回复:返回空串,微信视为处理成功(不再重试)
|
||||
c.Ctx.Output.Body([]byte(""))
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
replyBody, berr := wechatmini.BuildTextReply(cfg.DataFormat, msg.FromUserName, msg.ToUserName, reply, now)
|
||||
if berr != nil {
|
||||
c.Ctx.Output.Body([]byte(""))
|
||||
return
|
||||
}
|
||||
if encrypted {
|
||||
ts := q.Get("timestamp")
|
||||
if ts == "" {
|
||||
ts = msg.TimeStamp
|
||||
}
|
||||
replyNonce := nonce
|
||||
if replyNonce == "" {
|
||||
replyNonce = msg.Nonce
|
||||
}
|
||||
enc, eerr := wechatmini.BuildEncryptedReply(cfg, replyBody, ts, replyNonce, cfg.DataFormat)
|
||||
if eerr != nil {
|
||||
c.Ctx.Output.Body([]byte(""))
|
||||
return
|
||||
}
|
||||
replyBody = enc
|
||||
}
|
||||
|
||||
if cfg.DataFormat == wechatmini.DataFormatJSON {
|
||||
c.Ctx.Output.Header("Content-Type", "application/json; charset=utf-8")
|
||||
} else {
|
||||
c.Ctx.Output.Header("Content-Type", "application/xml; charset=utf-8")
|
||||
}
|
||||
c.Ctx.Output.Body([]byte(replyBody))
|
||||
}
|
||||
|
||||
// parsePushBody 按数据格式解析推送体
|
||||
func parsePushBody(raw, format string) (*wechatmini.PushMessage, error) {
|
||||
if format == wechatmini.DataFormatJSON {
|
||||
return wechatmini.ParsePushJSON(raw)
|
||||
}
|
||||
return wechatmini.ParsePushXML(raw)
|
||||
}
|
||||
|
||||
// verifyPushEchostr 校验微信「服务器地址验证」请求(GET),返回应回显的内容。
|
||||
//
|
||||
// - 明文 / 兼容模式:校验 sha1(sort(token,timestamp,nonce)) == signature,通过后原样回显 echostr
|
||||
// - 安全模式:echostr 为密文,校验 msg_signature 后解密,回显解密后的明文
|
||||
//
|
||||
// 命中任意一个已配置的小程序即视为验证通过(微信回调不携带 appid)。
|
||||
func verifyPushEchostr(cfgs []wechatmini.PushConfig, timestamp, nonce, signature, msgSignature, echostr string) (string, bool) {
|
||||
if echostr == "" {
|
||||
return "", false
|
||||
}
|
||||
// 安全模式优先
|
||||
if cfg, ok := wechatmini.MatchPushConfigByMsgSignature(cfgs, timestamp, nonce, echostr, msgSignature); ok &&
|
||||
cfg.EncryptMode == wechatmp.EncryptModeSafe {
|
||||
if plain, derr := wechatmini.DecryptPushMessage(cfg, echostr); derr == nil {
|
||||
return plain, true
|
||||
}
|
||||
}
|
||||
// 明文 / 兼容模式
|
||||
if _, ok := wechatmini.MatchPushConfigBySignature(cfgs, timestamp, nonce, signature); ok {
|
||||
return echostr, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"server/services/wechatmini"
|
||||
"server/services/wechatmp"
|
||||
)
|
||||
|
||||
// sign 复刻微信签名算法:sha1(sort(token, timestamp, nonce))
|
||||
func sign(token, timestamp, nonce string) string {
|
||||
arr := []string{token, timestamp, nonce}
|
||||
sort.Strings(arr)
|
||||
sum := sha1.Sum([]byte(strings.Join(arr, "")))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// signMsg 复刻加密消息签名:sha1(sort(token, timestamp, nonce, encrypt))
|
||||
func signMsg(token, timestamp, nonce, encrypt string) string {
|
||||
arr := []string{token, timestamp, nonce, encrypt}
|
||||
sort.Strings(arr)
|
||||
sum := sha1.Sum([]byte(strings.Join(arr, "")))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// 明文 / 兼容模式:签名正确时原样回显 echostr
|
||||
func TestVerifyPushEchostrPlain(t *testing.T) {
|
||||
cfgs := []wechatmini.PushConfig{
|
||||
{AppID: "wxApp", Token: "tokenPlain", EncryptMode: wechatmp.EncryptModePlain, DataFormat: wechatmini.DataFormatXML},
|
||||
}
|
||||
ts, nonce, echostr := "1700000000", "abc123", "hello-wechat"
|
||||
|
||||
got, ok := verifyPushEchostr(cfgs, ts, nonce, sign("tokenPlain", ts, nonce), "", echostr)
|
||||
if !ok {
|
||||
t.Fatal("明文模式签名正确,应验证通过")
|
||||
}
|
||||
if got != echostr {
|
||||
t.Fatalf("应原样回显 echostr,实际 %q", got)
|
||||
}
|
||||
|
||||
if _, ok := verifyPushEchostr(cfgs, ts, nonce, "bad-signature", "", echostr); ok {
|
||||
t.Fatal("错误签名不应通过")
|
||||
}
|
||||
if _, ok := verifyPushEchostr(cfgs, ts, nonce, sign("tokenPlain", ts, nonce), "", ""); ok {
|
||||
t.Fatal("缺少 echostr 不应通过")
|
||||
}
|
||||
}
|
||||
|
||||
// 安全模式:echostr 为密文,验签 + 解密后回显明文
|
||||
func TestVerifyPushEchostrSafeMode(t *testing.T) {
|
||||
key := wechatmini.GeneratePushAESKey()
|
||||
cfgs := []wechatmini.PushConfig{
|
||||
{AppID: "wxApp", Token: "tokenSafe", AESKey: key, EncryptMode: wechatmp.EncryptModeSafe, DataFormat: wechatmini.DataFormatXML},
|
||||
}
|
||||
ts, nonce := "1700000000", "abc123"
|
||||
plain := "encrypted-echo-str"
|
||||
encrypt, err := wechatmp.EncryptMessage(key, "wxApp", plain)
|
||||
if err != nil {
|
||||
t.Fatalf("加密失败: %v", err)
|
||||
}
|
||||
|
||||
got, ok := verifyPushEchostr(cfgs, ts, nonce, "", signMsg("tokenSafe", ts, nonce, encrypt), encrypt)
|
||||
if !ok {
|
||||
t.Fatal("安全模式签名正确,应验证通过")
|
||||
}
|
||||
if got != plain {
|
||||
t.Fatalf("应回显解密后的明文,实际 %q", got)
|
||||
}
|
||||
|
||||
if _, ok := verifyPushEchostr(cfgs, ts, nonce, "", "bad-msg-signature", encrypt); ok {
|
||||
t.Fatal("错误 msg_signature 不应通过")
|
||||
}
|
||||
}
|
||||
@@ -208,3 +208,235 @@ func (c *PlatformWechatMiniController) DeleteApp() {
|
||||
}
|
||||
c.ok(nil, "已删除")
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// 消息推送配置(小程序后台「开发管理 → 开发设置 → 消息推送」)
|
||||
// =============================================================
|
||||
|
||||
// GetPushConfig GET /platform/wechatMini/app/push?app_id=xxx
|
||||
// 返回该小程序的推送参数与「回调地址」(填到微信后台 URL),AESKey 掩码回显
|
||||
func (c *PlatformWechatMiniController) GetPushConfig() {
|
||||
if _, err := c.claims(); err != nil {
|
||||
code := identity.Status(err)
|
||||
c.jsonErr(code, code, err.Error())
|
||||
return
|
||||
}
|
||||
appID := strings.TrimSpace(c.GetString("app_id"))
|
||||
if appID == "" {
|
||||
c.jsonErr(400, 400, "缺少 app_id")
|
||||
return
|
||||
}
|
||||
cfg, err := wechatmini.LoadPushConfigByAppID(appID)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "小程序不存在或消息推送未配置")
|
||||
return
|
||||
}
|
||||
c.ok(map[string]interface{}{
|
||||
"app_id": cfg.AppID,
|
||||
"token": cfg.Token,
|
||||
"aes_key": wechatmp.MaskSecret(cfg.AESKey),
|
||||
"encrypt_mode": cfg.EncryptMode,
|
||||
"data_format": cfg.DataFormat,
|
||||
"push_reply": cfg.ReplyText,
|
||||
"callback_url": wechatmini.PushCallbackURL(),
|
||||
"configured": cfg.Token != "",
|
||||
}, "获取成功")
|
||||
}
|
||||
|
||||
// SavePushConfig POST /platform/wechatMini/app/push/save
|
||||
func (c *PlatformWechatMiniController) SavePushConfig() {
|
||||
if _, err := c.claims(); err != nil {
|
||||
code := identity.Status(err)
|
||||
c.jsonErr(code, code, err.Error())
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
AppID string `json:"app_id"`
|
||||
Token string `json:"token"`
|
||||
AESKey string `json:"aes_key"`
|
||||
EncryptMode string `json:"encrypt_mode"`
|
||||
DataFormat string `json:"data_format"`
|
||||
PushReply string `json:"push_reply"`
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.AppID) == "" {
|
||||
c.jsonErr(400, 400, "缺少 app_id")
|
||||
return
|
||||
}
|
||||
if err := wechatmini.SavePushConfig(wechatmini.SavePushConfigInput{
|
||||
AppID: p.AppID,
|
||||
Token: p.Token,
|
||||
AESKey: p.AESKey,
|
||||
EncryptMode: p.EncryptMode,
|
||||
DataFormat: p.DataFormat,
|
||||
ReplyText: p.PushReply,
|
||||
}); err != nil {
|
||||
c.jsonErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(map[string]interface{}{"callback_url": wechatmini.PushCallbackURL()}, "保存成功")
|
||||
}
|
||||
|
||||
// GeneratePushSecret POST /platform/wechatMini/app/push/generate
|
||||
// 生成随机 Token 与 EncodingAESKey(43 位字母数字)
|
||||
func (c *PlatformWechatMiniController) GeneratePushSecret() {
|
||||
if _, err := c.claims(); err != nil {
|
||||
code := identity.Status(err)
|
||||
c.jsonErr(code, code, err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(map[string]interface{}{
|
||||
"token": wechatmini.GeneratePushToken(),
|
||||
"aes_key": wechatmini.GeneratePushAESKey(),
|
||||
}, "生成成功")
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// 订阅消息模板(小程序后台「功能 → 订阅消息」选用模板后在此登记)
|
||||
// =============================================================
|
||||
|
||||
// ListTemplates GET /platform/wechatMini/templates?app_id=xxx
|
||||
func (c *PlatformWechatMiniController) ListTemplates() {
|
||||
if _, err := c.claims(); err != nil {
|
||||
code := identity.Status(err)
|
||||
c.jsonErr(code, code, err.Error())
|
||||
return
|
||||
}
|
||||
appID := strings.TrimSpace(c.GetString("app_id"))
|
||||
rows, err := wechatmini.ListTemplates(appID)
|
||||
if err != nil {
|
||||
c.jsonErr(500, 500, "获取模板列表失败")
|
||||
return
|
||||
}
|
||||
c.ok(map[string]interface{}{"list": rows}, "获取成功")
|
||||
}
|
||||
|
||||
// SaveTemplate POST /platform/wechatMini/template/save
|
||||
func (c *PlatformWechatMiniController) SaveTemplate() {
|
||||
if _, err := c.claims(); err != nil {
|
||||
code := identity.Status(err)
|
||||
c.jsonErr(code, code, err.Error())
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ID uint64 `json:"id"`
|
||||
AppID string `json:"app_id"`
|
||||
Code string `json:"code"`
|
||||
TemplateID string `json:"template_id"`
|
||||
Title string `json:"title"`
|
||||
Page string `json:"page"`
|
||||
Fields string `json:"fields"`
|
||||
Enabled int `json:"enabled"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := wechatmini.SaveTemplate(wechatmini.TemplateInput{
|
||||
ID: p.ID,
|
||||
AppID: p.AppID,
|
||||
Code: p.Code,
|
||||
TemplateID: p.TemplateID,
|
||||
Title: p.Title,
|
||||
Page: p.Page,
|
||||
Fields: p.Fields,
|
||||
Enabled: p.Enabled == 1,
|
||||
Remark: p.Remark,
|
||||
}); err != nil {
|
||||
c.jsonErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(nil, "保存成功")
|
||||
}
|
||||
|
||||
// DeleteTemplate POST /platform/wechatMini/template/delete/:id
|
||||
func (c *PlatformWechatMiniController) DeleteTemplate() {
|
||||
if _, err := c.claims(); err != nil {
|
||||
code := identity.Status(err)
|
||||
c.jsonErr(code, code, err.Error())
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.jsonErr(400, 400, "无效ID")
|
||||
return
|
||||
}
|
||||
if err := wechatmini.DeleteTemplate(id); err != nil {
|
||||
c.jsonErr(400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
c.ok(nil, "已删除")
|
||||
}
|
||||
|
||||
// TestTemplate POST /platform/wechatMini/template/test
|
||||
// 给指定租户用户发一条测试订阅消息,验证模板与字段映射是否正确
|
||||
func (c *PlatformWechatMiniController) TestTemplate() {
|
||||
if _, err := c.claims(); err != nil {
|
||||
code := identity.Status(err)
|
||||
c.jsonErr(code, code, err.Error())
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
TenantUserID uint64 `json:"tenant_user_id"`
|
||||
AppID string `json:"app_id"`
|
||||
Code string `json:"code"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.jsonErr(400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.TenantUserID == 0 {
|
||||
c.jsonErr(400, 400, "请指定测试用户(租户用户ID)")
|
||||
return
|
||||
}
|
||||
code := strings.TrimSpace(p.Code)
|
||||
if code == "" {
|
||||
code = wechatmini.SceneNotice
|
||||
}
|
||||
title := strings.TrimSpace(p.Title)
|
||||
if title == "" {
|
||||
title = "测试提醒"
|
||||
}
|
||||
content := strings.TrimSpace(p.Content)
|
||||
if content == "" {
|
||||
content = "这是一条订阅消息测试,收到说明配置正确。"
|
||||
}
|
||||
res := wechatmini.PushSubscribeToTenantUser(p.TenantUserID, code, strings.TrimSpace(p.AppID), map[string]string{
|
||||
"title": title,
|
||||
"content": content,
|
||||
})
|
||||
if res.Sent > 0 {
|
||||
c.ok(map[string]interface{}{"sent": res.Sent}, "已发送")
|
||||
return
|
||||
}
|
||||
if len(res.Errors) > 0 {
|
||||
msg := res.Errors[0].Error()
|
||||
if hint := wechatmini.ExplainSubscribeError(res.Errors[0]); hint != "" {
|
||||
msg = hint
|
||||
}
|
||||
c.jsonErr(400, 400, msg)
|
||||
return
|
||||
}
|
||||
c.jsonErr(400, 400, "未发送:该用户没有授权订阅,或模板未配置/未启用")
|
||||
}
|
||||
Reference in new issue
Block a user