443 lines
12 KiB
Go
443 lines
12 KiB
Go
package controllers
|
||
|
||
import (
|
||
"encoding/json"
|
||
"io"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"server/models"
|
||
"server/pkg/identity"
|
||
"server/pkg/jwtutil"
|
||
"server/services/wechatmini"
|
||
"server/services/wechatmp"
|
||
|
||
beego "github.com/beego/beego/v2/server/web"
|
||
)
|
||
|
||
// =============================================================
|
||
// 平台端「微信小程序」配置:AppID / AppSecret / 启用开关 / 自动绑定
|
||
//
|
||
// 配置就绪后,uniapp 小程序端的登录页才会展示「微信手机号一键登录」入口
|
||
// (GET /auth/wechat/miniapp/config 返回 enabled)。
|
||
// AppSecret 掩码回显,提交空串或掩码表示不修改。
|
||
// =============================================================
|
||
|
||
// PlatformWechatMiniController 平台端微信小程序配置
|
||
type PlatformWechatMiniController struct {
|
||
beego.Controller
|
||
}
|
||
|
||
func (c *PlatformWechatMiniController) claims() (*jwtutil.Claims, error) {
|
||
return identity.Claims(c.Ctx.Request.Header.Get("Authorization"), identity.EndPlatform)
|
||
}
|
||
|
||
func (c *PlatformWechatMiniController) 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 *PlatformWechatMiniController) 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()
|
||
}
|
||
|
||
// GetConfig GET /platform/wechatMini/config
|
||
func (c *PlatformWechatMiniController) GetConfig() {
|
||
if _, err := c.claims(); err != nil {
|
||
code := identity.Status(err)
|
||
c.jsonErr(code, code, err.Error())
|
||
return
|
||
}
|
||
data := map[string]interface{}{
|
||
"app_id": "",
|
||
"app_secret": "",
|
||
"enabled": 0,
|
||
"auto_bind": 1,
|
||
"remark": "",
|
||
"configured": false,
|
||
}
|
||
if cfg, err := wechatmini.LoadConfig(); err == nil {
|
||
data["app_id"] = cfg.AppID
|
||
data["app_secret"] = wechatmp.MaskSecret(cfg.AppSecret)
|
||
data["remark"] = cfg.Remark
|
||
data["configured"] = cfg.AppID != "" && cfg.AppSecret != ""
|
||
if cfg.Enabled {
|
||
data["enabled"] = 1
|
||
}
|
||
if cfg.AutoBind {
|
||
data["auto_bind"] = 1
|
||
} else {
|
||
data["auto_bind"] = 0
|
||
}
|
||
}
|
||
c.ok(data, "获取成功")
|
||
}
|
||
|
||
// SaveConfig POST /platform/wechatMini/config
|
||
func (c *PlatformWechatMiniController) SaveConfig() {
|
||
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"`
|
||
AppSecret string `json:"app_secret"`
|
||
Enabled int `json:"enabled"`
|
||
AutoBind int `json:"auto_bind"`
|
||
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.SaveConfig(wechatmini.SaveInput{
|
||
AppID: p.AppID,
|
||
AppSecret: p.AppSecret,
|
||
Enabled: p.Enabled == 1,
|
||
AutoBind: p.AutoBind == 1,
|
||
Remark: p.Remark,
|
||
}); err != nil {
|
||
c.jsonErr(400, 400, err.Error())
|
||
return
|
||
}
|
||
c.ok(nil, "保存成功")
|
||
}
|
||
|
||
// ListApps GET /platform/wechatMini/apps 多小程序应用列表(AppSecret 不回显)
|
||
func (c *PlatformWechatMiniController) ListApps() {
|
||
if _, err := c.claims(); err != nil {
|
||
code := identity.Status(err)
|
||
c.jsonErr(code, code, err.Error())
|
||
return
|
||
}
|
||
rows, err := wechatmini.ListApps()
|
||
if err != nil {
|
||
c.jsonErr(500, 500, "获取应用列表失败")
|
||
return
|
||
}
|
||
type appDTO struct {
|
||
models.WechatMiniApp
|
||
Configured bool `json:"configured"` // AppSecret 已配置
|
||
}
|
||
list := make([]appDTO, 0, len(rows))
|
||
for _, r := range rows {
|
||
dto := appDTO{WechatMiniApp: r, Configured: r.AppSecret != ""}
|
||
list = append(list, dto)
|
||
}
|
||
c.ok(map[string]interface{}{"list": list}, "获取成功")
|
||
}
|
||
|
||
// SaveApp POST /platform/wechatMini/app/save 新增/更新应用
|
||
func (c *PlatformWechatMiniController) SaveApp() {
|
||
if _, err := c.claims(); err != nil {
|
||
code := identity.Status(err)
|
||
c.jsonErr(code, code, err.Error())
|
||
return
|
||
}
|
||
var p struct {
|
||
ID uint64 `json:"id"`
|
||
Name string `json:"name"`
|
||
AppID string `json:"app_id"`
|
||
AppSecret string `json:"app_secret"`
|
||
QrcodeURL string `json:"qrcode_url"`
|
||
Enabled int `json:"enabled"`
|
||
AutoBind int `json:"auto_bind"`
|
||
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 strings.TrimSpace(p.Name) == "" {
|
||
c.jsonErr(400, 400, "应用名称不能为空")
|
||
return
|
||
}
|
||
id, err := wechatmini.SaveApp(wechatmini.SaveAppInput{
|
||
ID: p.ID,
|
||
Name: p.Name,
|
||
AppID: p.AppID,
|
||
AppSecret: p.AppSecret,
|
||
QrcodeURL: p.QrcodeURL,
|
||
Enabled: p.Enabled == 1,
|
||
AutoBind: p.AutoBind == 1,
|
||
Remark: p.Remark,
|
||
})
|
||
if err != nil {
|
||
c.jsonErr(400, 400, err.Error())
|
||
return
|
||
}
|
||
c.ok(map[string]interface{}{"id": id}, "保存成功")
|
||
}
|
||
|
||
// DeleteApp POST /platform/wechatMini/app/delete/:id 软删应用并停用
|
||
func (c *PlatformWechatMiniController) DeleteApp() {
|
||
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.DeleteApp(id); err != nil {
|
||
c.jsonErr(400, 400, err.Error())
|
||
return
|
||
}
|
||
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, "未发送:该用户没有授权订阅,或模板未配置/未启用")
|
||
}
|