批量优化功能
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
package wechatmp
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// =============================================================
|
||||
// 公众号微信登录(免认证方案):
|
||||
// 用户在公众号内发送「登录」→ 后端校验绑定关系并生成一次性登录令牌 →
|
||||
// 被动回复登录链接 → 用户在微信内打开链接 → 前端 /mp-login 页面
|
||||
// 用令牌调用 /api/wechat/mp/login 换取正式登录态。
|
||||
// 未认证公众号无法使用网页授权(snsapi_*),一次性链接是替代方案。
|
||||
// =============================================================
|
||||
|
||||
// DefaultLoginTokenTTL 登录令牌默认有效期
|
||||
const DefaultLoginTokenTTL = 5 * time.Minute
|
||||
|
||||
var (
|
||||
ErrLoginTokenInvalid = errors.New("登录链接无效或已过期,请在公众号重新发送「登录」")
|
||||
ErrLoginTokenUsed = errors.New("登录链接已被使用,请在公众号重新发送「登录」")
|
||||
ErrLoginTokenExpired = errors.New("登录链接已过期,请在公众号重新发送「登录」")
|
||||
)
|
||||
|
||||
func randomTokenHex(nBytes int) string {
|
||||
b := make([]byte, nBytes)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return ""
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// LoginLinkURL 由一次性令牌拼出前端登录页链接。
|
||||
// 页面基址读取 app.conf 的 wechat_mp_login_base(前端应用对外可访问地址,
|
||||
// 如 https://back.yunzer.cn),缺省回落 wechat_mp_callback_base / payment_callback_base。
|
||||
// 未配置时返回空串,由调用方给出提示。
|
||||
func LoginLinkURL(token string) string {
|
||||
base, _ := beego.AppConfig.String("wechat_mp_login_base")
|
||||
if strings.TrimSpace(base) == "" {
|
||||
base, _ = beego.AppConfig.String("wechat_mp_callback_base")
|
||||
}
|
||||
if strings.TrimSpace(base) == "" {
|
||||
base, _ = beego.AppConfig.String("payment_callback_base")
|
||||
}
|
||||
base = strings.TrimRight(strings.TrimSpace(base), "/")
|
||||
if base == "" {
|
||||
return ""
|
||||
}
|
||||
return base + "/#/mp-login?token=" + url.QueryEscape(token)
|
||||
}
|
||||
|
||||
// CreateLoginToken 为已绑定的粉丝生成一次性登录令牌。
|
||||
func CreateLoginToken(f *models.WechatMpFollower, ttl time.Duration) (token string, expireAt time.Time, err error) {
|
||||
if f == nil || strings.TrimSpace(f.BindType) == "" || f.BindID == 0 {
|
||||
return "", time.Time{}, errors.New("该微信尚未绑定账号")
|
||||
}
|
||||
if ttl <= 0 {
|
||||
ttl = DefaultLoginTokenTTL
|
||||
}
|
||||
token = randomTokenHex(32)
|
||||
if token == "" {
|
||||
return "", time.Time{}, errors.New("生成登录令牌失败")
|
||||
}
|
||||
now := time.Now()
|
||||
expireAt = now.Add(ttl)
|
||||
row := &models.WechatMpLoginToken{
|
||||
Token: token,
|
||||
OpenID: f.OpenID,
|
||||
BindType: f.BindType,
|
||||
BindID: f.BindID,
|
||||
BindTid: f.BindTid,
|
||||
ExpireAt: expireAt,
|
||||
CreateTime: now,
|
||||
}
|
||||
if _, err := models.Orm.Insert(row); err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
return token, expireAt, nil
|
||||
}
|
||||
|
||||
// ConsumeLoginToken 核销一次性登录令牌(CAS 防重放),返回令牌携带的绑定信息。
|
||||
func ConsumeLoginToken(token string) (*models.WechatMpLoginToken, error) {
|
||||
token = strings.TrimSpace(token)
|
||||
if token == "" {
|
||||
return nil, ErrLoginTokenInvalid
|
||||
}
|
||||
var row models.WechatMpLoginToken
|
||||
if err := models.Orm.QueryTable(new(models.WechatMpLoginToken)).
|
||||
Filter("token", token).One(&row); err != nil {
|
||||
return nil, ErrLoginTokenInvalid
|
||||
}
|
||||
if row.UsedAt != nil {
|
||||
return nil, ErrLoginTokenUsed
|
||||
}
|
||||
if row.ExpireAt.Before(time.Now()) {
|
||||
return nil, ErrLoginTokenExpired
|
||||
}
|
||||
now := time.Now()
|
||||
affected, err := models.Orm.QueryTable(new(models.WechatMpLoginToken)).
|
||||
Filter("id", row.ID).
|
||||
Filter("used_at__isnull", true).
|
||||
Update(map[string]interface{}{"used_at": now})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if affected == 0 {
|
||||
return nil, ErrLoginTokenUsed
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// isLoginKeyword 判断文本是否为「登录」意图
|
||||
func isLoginKeyword(text string) bool {
|
||||
switch strings.TrimSpace(text) {
|
||||
case "登录", "登陸", "login", "Login", "微信登录", "微信登陆":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isLoginMenuKey 判断自定义菜单点击(CLICK 事件)的 EventKey 是否为「登录」入口。
|
||||
// 菜单在公众号后台手动配置即可(未认证公众号同样支持),
|
||||
// 用户点菜单 → CLICK 事件 → 自动回复确认登录链接,免输入。
|
||||
func isLoginMenuKey(key string) bool {
|
||||
k := strings.TrimSpace(key)
|
||||
if k == "" {
|
||||
return false
|
||||
}
|
||||
switch strings.ToLower(k) {
|
||||
case "login", "登录", "扫码登录", "微信登录":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user