Files
yunzerwebsiteallinone/go/services/wechatmp/register.go
T
2026-10-31 01:21:55 +08:00

219 lines
8.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}