219 lines
8.0 KiB
Go
219 lines
8.0 KiB
Go
package wechatmp
|
||
|
||
import (
|
||
"errors"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/beego/beego/v2/core/logs"
|
||
|
||
"server/models"
|
||
)
|
||
|
||
// =============================================================
|
||
// 注册「账号注册」验证码:网页发起 → 展示带参二维码(scene)→
|
||
// 用户微信扫码关注 → 服务号被动回复 6 位验证码 → 网页填码核销。
|
||
// 复用 yz_platform_wechat_mp_verify_code 会话表(bind_type="register"),
|
||
// 扫码事件回复走通用 handleScanScene,无需新增事件处理。
|
||
// =============================================================
|
||
|
||
// WechatBindTypeRegister 注册验证码会话类型(yz_platform_wechat_mp_verify_code.bind_type)
|
||
const WechatBindTypeRegister = "register"
|
||
|
||
// ErrRegisterCodeInvalid 账号注册:验证码无效 / 已使用(引导语指向「注册」而非绑定用的「验证码」)
|
||
var ErrRegisterCodeInvalid = errors.New("验证码不正确或已过期:请在公众号内重新发送「注册」获取")
|
||
|
||
// RegisterQRDownNotice 带参二维码不可用(IP 白名单 40164 / 未认证 48001 等)时,
|
||
// 回显给注册页的用户级引导:详细原因见服务端日志,不暴露服务器出口 IP。
|
||
const RegisterQRDownNotice = "二维码暂不可用:请在公众号内直接发送「注册」获取验证码"
|
||
|
||
// StartRegisterVerify 创建注册验证码会话并生成带参二维码。
|
||
// 带参二维码生成失败(未认证公众号 48001 等)时回退:qrURL 为空、
|
||
// notice 返回引导(用户在公众号内发送「注册」获取验证码),scene 仍有效可轮询。
|
||
func StartRegisterVerify(ttl time.Duration) (scene, qrURL, notice string, expireAt time.Time, err error) {
|
||
if ttl <= 0 {
|
||
ttl = DefaultVerifyTTL
|
||
}
|
||
cfg, err := LoadEnabledConfig()
|
||
if err != nil {
|
||
return "", "", "", time.Time{}, err
|
||
}
|
||
scene = randomScene()
|
||
code := randomCode6()
|
||
now := time.Now()
|
||
expireAt = now.Add(ttl)
|
||
|
||
row := &models.WechatMpVerifyCode{
|
||
Scene: scene,
|
||
Code: code,
|
||
Status: models.WechatVerifyStatusWaiting,
|
||
BindType: WechatBindTypeRegister,
|
||
ExpireAt: &expireAt,
|
||
CreateTime: now,
|
||
UpdateTime: &now,
|
||
}
|
||
if _, err = models.Orm.Insert(row); err != nil {
|
||
return "", "", "", time.Time{}, err
|
||
}
|
||
qrURL, err = CreateQRCode(cfg, scene, int(ttl.Seconds())+120)
|
||
if err != nil {
|
||
// 注册页匿名可访问:errcode / errmsg / 服务器出口 IP 等排查信息已在
|
||
// CreateQRCode 内落到服务端日志(见 logAPIError),此处只回显用户级引导,
|
||
// 避免向访客泄露服务器出口 IP 与内部错误细节;
|
||
// 会话仍然有效,「公众号内发送注册」这条兜底路径可正常核销。
|
||
logs.Warning("[wechat-mp] 注册二维码不可用,已回退公众号发送「注册」:%v", err)
|
||
return scene, "", RegisterQRDownNotice, expireAt, nil
|
||
}
|
||
return scene, qrURL, "", expireAt, nil
|
||
}
|
||
|
||
// ConfirmAccountRegisterVerify 账号注册的验证码核销:
|
||
// 优先按 scene 核销二维码会话;该会话尚未扫码(用户没扫码,改为在公众号内发送
|
||
// 「注册」取码)、scene 失效或验证码不属于该会话时,直接按验证码认领会话。
|
||
//
|
||
// 账号注册不绑定手机号,服务号无法按手机号把「发送者」与网页会话关联起来,
|
||
// 因此不要求网页会话必须先被扫码:验证码有效即视为公众号内本人操作。
|
||
func ConfirmAccountRegisterVerify(scene, code string) (string, error) {
|
||
code = strings.TrimSpace(code)
|
||
if code == "" {
|
||
return "", ErrCodeNotPending
|
||
}
|
||
if strings.TrimSpace(scene) != "" {
|
||
if openID, err := ConfirmRegisterVerify(scene, code); err == nil {
|
||
return openID, nil
|
||
}
|
||
}
|
||
openID, err := ConfirmRegisterVerifyByCode(code)
|
||
if err != nil {
|
||
// 按码认领失败的提示要指向注册场景(发送「注册」),而非绑定场景的「验证码」
|
||
if errors.Is(err, ErrCodeNotPending) || errors.Is(err, ErrCodeUsed) {
|
||
return "", ErrRegisterCodeInvalid
|
||
}
|
||
return "", err
|
||
}
|
||
return openID, nil
|
||
}
|
||
|
||
// GetRegisterVerifyStatus 查询注册会话扫码状态(网页轮询展示「已扫码」)
|
||
func GetRegisterVerifyStatus(scene string) (scanned bool, expireAt time.Time, err error) {
|
||
row, err := GetVerifyCode(scene)
|
||
if err != nil {
|
||
return false, time.Time{}, err
|
||
}
|
||
if row.ExpireAt != nil {
|
||
expireAt = *row.ExpireAt
|
||
}
|
||
if row.BindType != WechatBindTypeRegister {
|
||
return false, expireAt, ErrSceneInvalid
|
||
}
|
||
return row.Status == models.WechatVerifyStatusScanned, expireAt, nil
|
||
}
|
||
|
||
// ConfirmRegisterVerify 核销注册验证码:校验会话归属与状态,返回扫码人 openid。
|
||
// 要求会话已被扫码(openid 已写入)、验证码匹配,核销后一次性失效。
|
||
func ConfirmRegisterVerify(scene, code string) (openid string, err error) {
|
||
row, err := GetVerifyCode(scene)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if row.BindType != WechatBindTypeRegister {
|
||
return "", ErrSceneInvalid
|
||
}
|
||
switch row.Status {
|
||
case models.WechatVerifyStatusWaiting:
|
||
return "", ErrSceneNotScanned
|
||
case models.WechatVerifyStatusUsed:
|
||
return "", ErrCodeUsed
|
||
case models.WechatVerifyStatusExpired:
|
||
return "", ErrCodeExpired
|
||
}
|
||
if strings.TrimSpace(code) != row.Code {
|
||
return "", ErrCodeNotPending
|
||
}
|
||
now := time.Now()
|
||
if _, uerr := models.Orm.QueryTable(new(models.WechatMpVerifyCode)).
|
||
Filter("id", row.ID).
|
||
Filter("status", models.WechatVerifyStatusScanned).
|
||
Update(map[string]interface{}{"status": models.WechatVerifyStatusUsed, "update_time": now}); uerr != nil {
|
||
return "", uerr
|
||
}
|
||
return row.OpenID, nil
|
||
}
|
||
|
||
// CreateRegisterOpenIDVerify 公众号内发送「注册」时创建的注册会话:
|
||
// openid 已知(消息发送者),网页端按验证码核销(无带参二维码的兜底路径)。
|
||
func CreateRegisterOpenIDVerify(openid string, ttl time.Duration) (*models.WechatMpVerifyCode, error) {
|
||
openid = strings.TrimSpace(openid)
|
||
if openid == "" {
|
||
return nil, errors.New("openid 为空")
|
||
}
|
||
if ttl <= 0 {
|
||
ttl = DefaultVerifyTTL
|
||
}
|
||
now := time.Now()
|
||
expireAt := now.Add(ttl)
|
||
row := &models.WechatMpVerifyCode{
|
||
Scene: randomScene(),
|
||
Code: randomCode6(),
|
||
OpenID: openid,
|
||
Status: models.WechatVerifyStatusScanned, // 微信号已知,等同「已扫码待核销」
|
||
BindType: WechatBindTypeRegister,
|
||
ExpireAt: &expireAt,
|
||
CreateTime: now,
|
||
UpdateTime: &now,
|
||
}
|
||
if _, err := models.Orm.Insert(row); err != nil {
|
||
return nil, err
|
||
}
|
||
return row, nil
|
||
}
|
||
|
||
// LatestPendingRegisterVerifyByOpenID 该微信号最近一条未核销的注册会话(重发验证码时复用)
|
||
func LatestPendingRegisterVerifyByOpenID(openid string) (*models.WechatMpVerifyCode, error) {
|
||
var row models.WechatMpVerifyCode
|
||
err := models.Orm.QueryTable(new(models.WechatMpVerifyCode)).
|
||
Filter("openid", openid).
|
||
Filter("bind_type", WechatBindTypeRegister).
|
||
Filter("status", models.WechatVerifyStatusScanned).
|
||
Filter("expire_at__gt", time.Now()).
|
||
OrderBy("-id").
|
||
One(&row)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &row, nil
|
||
}
|
||
|
||
// ConfirmRegisterVerifyByCode 无二维码兜底:按验证码认领「公众号发起」的注册会话。
|
||
// 只认领 bind_type=register、openid 非空、已扫码待核销的最近会话,CAS 核销防止重复使用。
|
||
func ConfirmRegisterVerifyByCode(code string) (string, error) {
|
||
code = strings.TrimSpace(code)
|
||
if code == "" {
|
||
return "", ErrCodeNotPending
|
||
}
|
||
var row models.WechatMpVerifyCode
|
||
err := models.Orm.QueryTable(new(models.WechatMpVerifyCode)).
|
||
Filter("code", code).
|
||
Filter("bind_type", WechatBindTypeRegister).
|
||
Filter("openid__ne", "").
|
||
Filter("status", models.WechatVerifyStatusScanned).
|
||
Filter("expire_at__gt", time.Now()).
|
||
OrderBy("-id").
|
||
One(&row)
|
||
if err != nil {
|
||
return "", ErrCodeNotPending
|
||
}
|
||
now := time.Now()
|
||
affected, uerr := models.Orm.QueryTable(new(models.WechatMpVerifyCode)).
|
||
Filter("id", row.ID).
|
||
Filter("status", models.WechatVerifyStatusScanned).
|
||
Update(map[string]interface{}{"status": models.WechatVerifyStatusUsed, "update_time": now})
|
||
if uerr != nil {
|
||
return "", uerr
|
||
}
|
||
if affected == 0 {
|
||
return "", ErrCodeUsed
|
||
}
|
||
return row.OpenID, nil
|
||
}
|