批量优化功能
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
package wechatmp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// =============================================================
|
||||
// 公众号扫码登录(带参二维码方案):
|
||||
// PC 端 /auth/login 扫码面板发起会话并轮询 → 展示带参二维码(scene=会话标识)→
|
||||
// 用户微信扫码:已关注触发 SCAN 事件 / 未关注关注后触发 subscribe 事件,
|
||||
// 服务号【自动】回复「确认登录」一次性链接(无需用户输入任何内容)→
|
||||
// 用户点击链接(/mp-login 兑换,链接携带 scene)→ 精确认领 PC 会话 →
|
||||
// PC 端轮询到确认后建立 UAC 会话自动登录。全程无极验 / 无登录码。
|
||||
// 注:带参二维码需认证公众号;未认证(48001)时回退普通关注二维码,
|
||||
// 用户在公众号发送「登录」换取确认链接。
|
||||
// =============================================================
|
||||
|
||||
// ScanLoginTTL 扫码登录会话有效期
|
||||
const ScanLoginTTL = 5 * time.Minute
|
||||
|
||||
var ErrScanLoginInvalid = errors.New("扫码登录会话不存在或已过期,请刷新二维码重试")
|
||||
|
||||
// StartScanLogin 发起 PC 扫码登录会话,并生成带参二维码。
|
||||
// 带参二维码生成失败(未认证公众号 48001 等)时回退普通关注二维码:
|
||||
// qrURL 为空、notice 返回回退原因,前端需明确提示用户走「发送登录」路径。
|
||||
func StartScanLogin() (scene, qrURL, notice string, expireAt time.Time, err error) {
|
||||
scene = randomScene()
|
||||
now := time.Now()
|
||||
expireAt = now.Add(ScanLoginTTL)
|
||||
row := &models.WechatMpScanLogin{
|
||||
Scene: scene,
|
||||
Code: "-",
|
||||
ExpireAt: expireAt,
|
||||
CreateTime: now,
|
||||
}
|
||||
if _, err = models.Orm.Insert(row); err != nil {
|
||||
return "", "", "", time.Time{}, err
|
||||
}
|
||||
cfg, cerr := LoadEnabledConfig()
|
||||
if cerr == nil {
|
||||
qr, qerr := CreateQRCode(cfg, scene, int(ScanLoginTTL.Seconds())+120)
|
||||
if qerr == nil {
|
||||
return scene, qr, "", expireAt, nil
|
||||
}
|
||||
var apiErr *APIError
|
||||
if errors.As(qerr, &apiErr) {
|
||||
notice = "公众号未返回带参二维码(" + apiErr.Error() + ")"
|
||||
if hint := apiErr.Explain(); hint != "" {
|
||||
notice += ":" + hint
|
||||
}
|
||||
} else {
|
||||
notice = "生成带参二维码失败:" + qerr.Error()
|
||||
}
|
||||
} else {
|
||||
notice = "公众号未配置或未启用,无法生成带参二维码"
|
||||
}
|
||||
notice += "。已回退普通二维码:已关注的用户扫码不会触发消息,请在公众号内发送「登录」获取确认链接。"
|
||||
return scene, "", notice, expireAt, nil
|
||||
}
|
||||
|
||||
// GetScanLogin 查询扫码登录会话(过期时惰性标记)
|
||||
func GetScanLogin(scene string) (*models.WechatMpScanLogin, error) {
|
||||
scene = strings.TrimSpace(scene)
|
||||
if scene == "" {
|
||||
return nil, ErrScanLoginInvalid
|
||||
}
|
||||
var row models.WechatMpScanLogin
|
||||
if err := models.Orm.QueryTable(new(models.WechatMpScanLogin)).
|
||||
Filter("scene", scene).One(&row); err != nil {
|
||||
return nil, ErrScanLoginInvalid
|
||||
}
|
||||
if row.Status == models.WechatScanLoginPending && row.ExpireAt.Before(time.Now()) {
|
||||
now := time.Now()
|
||||
_, _ = models.Orm.QueryTable(new(models.WechatMpScanLogin)).
|
||||
Filter("id", row.ID).
|
||||
Update(map[string]interface{}{"status": models.WechatScanLoginExpired, "update_time": now})
|
||||
row.Status = models.WechatScanLoginExpired
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// HandleScanLoginEvent 带参二维码扫码事件(SCAN / subscribe 带 qrscene):
|
||||
// scene 属于等待中的扫码登录会话时,自动回复「确认登录」链接(已绑定)
|
||||
// 或绑定提示(未绑定),并返回 true;不属于登录会话返回 false 交由绑定流程处理。
|
||||
func HandleScanLoginEvent(scene, openid string) (string, bool) {
|
||||
scene = strings.TrimSpace(scene)
|
||||
if scene == "" {
|
||||
return "", false
|
||||
}
|
||||
row, err := GetScanLogin(scene)
|
||||
if err != nil || row.Status != models.WechatScanLoginPending {
|
||||
return "", false
|
||||
}
|
||||
|
||||
f, ferr := GetFollowerByOpenID(openid)
|
||||
if ferr != nil || f == nil || strings.TrimSpace(f.BindType) == "" || f.BindID == 0 {
|
||||
return "当前微信尚未绑定账号,无法扫码登录:请先在系统页面完成「绑定微信」。", true
|
||||
}
|
||||
if f.BindType != models.WechatBindTypeTenantUser {
|
||||
return "该微信绑定的是平台账号,暂不支持扫码登录。", true
|
||||
}
|
||||
|
||||
token, _, terr := CreateLoginToken(f, ScanLoginTTL)
|
||||
if terr != nil {
|
||||
return "系统繁忙,请稍后重新扫码。", true
|
||||
}
|
||||
now := time.Now()
|
||||
if _, uerr := models.Orm.QueryTable(new(models.WechatMpScanLogin)).
|
||||
Filter("id", row.ID).
|
||||
Update(map[string]interface{}{
|
||||
"openid": openid,
|
||||
"login_token": token,
|
||||
"update_time": now,
|
||||
}); uerr != nil {
|
||||
return "系统繁忙,请稍后重新扫码。", true
|
||||
}
|
||||
|
||||
link := LoginLinkURL(token) + "&scene=" + url.QueryEscape(scene)
|
||||
minutes := 1
|
||||
if m := int(time.Until(now.Add(ScanLoginTTL)).Minutes()); m > 1 {
|
||||
minutes = m
|
||||
}
|
||||
return fmt.Sprintf("【扫码登录确认】\n点击以下链接确认登录(%d 分钟内有效,仅可使用一次):\n%s\n\n确认后您的电脑将自动登录。如非本人操作,请忽略本条消息。", minutes, link), true
|
||||
}
|
||||
|
||||
// DirectLoginReply 判断该微信的绑定状态并返回对应回复:
|
||||
// - 已绑定:直接给出「确认登录」一次性链接(点击后 web 直接登录);
|
||||
// - 未绑定:提示先去系统页面完成绑定。
|
||||
//
|
||||
// 用于普通关注(无 scene)与公众号内发送「登录」的兜底路径。
|
||||
func DirectLoginReply(openid string) string {
|
||||
f, err := GetFollowerByOpenID(openid)
|
||||
if err != nil || f == nil || strings.TrimSpace(f.BindType) == "" || f.BindID == 0 {
|
||||
return "当前微信尚未绑定账号:请先在系统页面「绑定微信」处完成绑定(关注后发送「验证码」,再到页面填码)。"
|
||||
}
|
||||
if f.BindType != models.WechatBindTypeTenantUser {
|
||||
return "该微信绑定的是平台账号,暂不支持扫码登录。"
|
||||
}
|
||||
token, expireAt, terr := CreateLoginToken(f, ScanLoginTTL)
|
||||
if terr != nil {
|
||||
return "系统繁忙,请稍后重新发送「登录」。"
|
||||
}
|
||||
link := LoginLinkURL(token)
|
||||
if link == "" {
|
||||
return "管理员尚未配置登录页地址(app.conf 的 wechat_mp_login_base),请联系管理员。"
|
||||
}
|
||||
minutes := 1
|
||||
if m := int(time.Until(expireAt).Minutes()); m > 1 {
|
||||
minutes = m
|
||||
}
|
||||
return fmt.Sprintf("【确认登录】\n点击以下链接直接登录(%d 分钟内有效,仅可使用一次):\n%s\n\n如非本人操作,请忽略本条消息。", minutes, link)
|
||||
}
|
||||
|
||||
// ConfirmScanLoginByToken 兑换确认链接时精确认领 PC 扫码会话(scene + 令牌双重匹配,CAS)
|
||||
func ConfirmScanLoginByToken(scene, loginToken, bindType string, bindID, bindTid uint64) {
|
||||
scene = strings.TrimSpace(scene)
|
||||
if scene == "" || strings.TrimSpace(loginToken) == "" {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, _ = models.Orm.QueryTable(new(models.WechatMpScanLogin)).
|
||||
Filter("scene", scene).
|
||||
Filter("login_token", strings.TrimSpace(loginToken)).
|
||||
Filter("status", models.WechatScanLoginPending).
|
||||
Update(map[string]interface{}{
|
||||
"status": models.WechatScanLoginOK,
|
||||
"bind_type": bindType,
|
||||
"bind_id": bindID,
|
||||
"bind_tid": bindTid,
|
||||
"update_time": now,
|
||||
})
|
||||
}
|
||||
|
||||
// ConfirmOldestPendingScanLogin 无 scene 的兜底认领(「登录」文本路径):
|
||||
// 认领最早一条待确认 PC 会话;没有等待中的会话时静默跳过。
|
||||
func ConfirmOldestPendingScanLogin(bindType string, bindID, bindTid uint64) {
|
||||
now := time.Now()
|
||||
var row models.WechatMpScanLogin
|
||||
err := models.Orm.QueryTable(new(models.WechatMpScanLogin)).
|
||||
Filter("status", models.WechatScanLoginPending).
|
||||
Filter("expire_at__gt", now).
|
||||
OrderBy("id").
|
||||
One(&row)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
models.Orm.QueryTable(new(models.WechatMpScanLogin)).
|
||||
Filter("id", row.ID).
|
||||
Filter("status", models.WechatScanLoginPending).
|
||||
Update(map[string]interface{}{
|
||||
"status": models.WechatScanLoginOK,
|
||||
"bind_type": bindType,
|
||||
"bind_id": bindID,
|
||||
"bind_tid": bindTid,
|
||||
"update_time": now,
|
||||
})
|
||||
}
|
||||
|
||||
// PickScanLogin PC 端领取确认结果:CAS 把「已确认」置为「已领取」,防止重复领取
|
||||
func PickScanLogin(scene string) (*models.WechatMpScanLogin, error) {
|
||||
row, err := GetScanLogin(scene)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if row.Status == models.WechatScanLoginPending {
|
||||
return nil, errors.New("等待微信确认:请在公众号内点击「确认登录」链接")
|
||||
}
|
||||
if row.Status == models.WechatScanLoginExpired {
|
||||
return nil, ErrScanLoginInvalid
|
||||
}
|
||||
if row.Status != models.WechatScanLoginOK || row.BindID == 0 {
|
||||
return nil, errors.New("该二维码已完成登录,请刷新后重新扫码")
|
||||
}
|
||||
now := time.Now()
|
||||
affected, err := models.Orm.QueryTable(new(models.WechatMpScanLogin)).
|
||||
Filter("id", row.ID).
|
||||
Filter("status", models.WechatScanLoginOK).
|
||||
Update(map[string]interface{}{"status": models.WechatScanLoginPicked, "update_time": now})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if affected == 0 {
|
||||
return nil, errors.New("该二维码已完成登录,请刷新后重新扫码")
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
Reference in New Issue
Block a user