批量优化功能

This commit is contained in:
2026-09-30 00:57:36 +08:00
parent ca4220f92c
commit e3db229f41
43 changed files with 2959 additions and 207 deletions
+133 -10
View File
@@ -1,9 +1,11 @@
package controllers
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"path/filepath"
"strings"
"time"
@@ -106,6 +108,7 @@ func (c *PlatformWechatMpController) GetConfig() {
data["encrypt_mode"] = cfg.EncryptMode
data["template_id"] = cfg.TemplateID
data["remark"] = cfg.Remark
data["follow_qrcode"] = cfg.FollowQrcode
data["configured"] = cfg.AppID != ""
if cfg.Verified {
data["verified"] = 1
@@ -190,7 +193,8 @@ func (c *PlatformWechatMpController) TestConnection() {
// ============================ 绑定(当前平台用户) ============================
// BindStart POST /platform/wechatMp/bind/start
// 生成带参二维码;用户扫码关注后公众号将被动回复验证码
// 未认证公众号无法生成带参二维码(48001):不再下发二维码,
// 返回绑定指引(公众号内发送「验证码」取码 → 页面填码 → bind/confirm 核销)。
func (c *PlatformWechatMpController) BindStart() {
claims, err := c.claims()
if err != nil {
@@ -202,18 +206,137 @@ func (c *PlatformWechatMpController) BindStart() {
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())
if _, err := wechatmp.LoadEnabledConfig(); 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(),
}, "生成成功")
"mode": "mp_message",
"guides": wechatmp.BindGuides(),
"follow_qrcode": wechatmp.FollowQrcodeURL(),
"expires_in": int(wechatmp.DefaultVerifyTTL.Seconds()),
"callback_url": wechatmp.CallbackURL(),
}, "请按指引完成绑定")
}
// ============================ 自定义菜单 ============================
// PublishMenu POST /platform/wechatMp/menu/publish
// 通过接口在公众号底部发布「扫码登录」菜单(点击 → 自动回复确认登录链接)。
// 需认证公众号;未认证(48001)时提示改用「公众号内发送登录」路径。
func (c *PlatformWechatMpController) PublishMenu() {
if _, err := c.claims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
cfg, err := wechatmp.LoadEnabledConfig()
if err != nil {
c.jsonErr(400, 400, "公众号未启用:"+err.Error())
return
}
res, merr := wechatmp.PublishLoginMenu(cfg)
if merr != nil {
msg := "发布菜单失败:" + merr.Error()
if hint, ok := wechatmp.ExplainError(merr); ok {
msg += ";" + hint
}
c.jsonErr(400, 400, msg)
return
}
// 微信客户端有菜单缓存:发布后需重新关注或等待(最长 24 小时)才可见
msg := "菜单已发布:微信客户端有缓存,请重新关注公众号或等待刷新(最长 24 小时)后可见「扫码登录」"
if res.AlreadyExists {
msg = "菜单中已存在「扫码登录」入口,未重复发布;若底部未显示,请等待客户端刷新或重新关注"
}
if res.ConditionalMenuCount > 0 {
msg += fmt.Sprintf(";检测到个性化菜单 %d 条,个性化菜单会覆盖默认菜单,请到公众号后台确认",
res.ConditionalMenuCount)
}
c.ok(map[string]interface{}{
"menu": res.Buttons,
"already_exists": res.AlreadyExists,
"conditional_menu_count": res.ConditionalMenuCount,
}, msg)
}
// ============================ 关注二维码 ============================
// 二维码图片限制:2MB,常见图片格式
const followQrcodeMaxBytes = 2 * 1024 * 1024
var followQrcodeAllowedExts = map[string]bool{
"jpg": true, "jpeg": true, "png": true, "gif": true, "webp": true, "bmp": true,
}
// saveFollowQrcode 校验图片并写入配置表(data URL 存储),返回 data URL
func (c *PlatformWechatMpController) saveFollowQrcode() (string, error) {
if err := c.Ctx.Request.ParseMultipartForm(followQrcodeMaxBytes); err != nil {
return "", fmt.Errorf("解析上传失败: %w", err)
}
fh, header, err := c.GetFile("file")
if err != nil || fh == nil {
return "", fmt.Errorf("请选择要上传的二维码图片")
}
defer fh.Close()
if header != nil && header.Size > followQrcodeMaxBytes {
return "", fmt.Errorf("图片大小不能超过 2MB")
}
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(header.Filename), "."))
if !followQrcodeAllowedExts[ext] {
return "", fmt.Errorf("仅支持 jpg / png / gif / webp / bmp 格式")
}
raw, err := io.ReadAll(fh)
if err != nil {
return "", fmt.Errorf("读取图片失败: %w", err)
}
if len(raw) == 0 {
return "", fmt.Errorf("图片内容为空")
}
mime := "image/png"
switch ext {
case "jpg", "jpeg":
mime = "image/jpeg"
case "gif":
mime = "image/gif"
case "webp":
mime = "image/webp"
case "bmp":
mime = "image/bmp"
}
return "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(raw), nil
}
// UploadFollowQrcode POST /platform/wechatMp/followQrcode
// 上传公众号「关注二维码」图片(普通二维码,用户扫码进入公众号会话)
func (c *PlatformWechatMpController) UploadFollowQrcode() {
if _, err := c.claims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
dataURL, err := c.saveFollowQrcode()
if err != nil {
c.jsonErr(400, 400, err.Error())
return
}
if err := wechatmp.SaveFollowQrcode(dataURL); err != nil {
c.jsonErr(500, 500, "保存失败:"+err.Error())
return
}
c.ok(map[string]interface{}{"follow_qrcode": dataURL}, "上传成功")
}
// DeleteFollowQrcode POST /platform/wechatMp/followQrcode/delete
func (c *PlatformWechatMpController) DeleteFollowQrcode() {
if _, err := c.claims(); err != nil {
c.jsonErr(401, 401, err.Error())
return
}
if err := wechatmp.SaveFollowQrcode(""); err != nil {
c.jsonErr(500, 500, "删除失败:"+err.Error())
return
}
c.ok(nil, "已删除")
}
// BindStatus GET /platform/wechatMp/bind/status?scene=xxx