Files
yunzerwebsiteallinone/go/services/wechatmp/register.go
T
2026-10-08 15:30:37 +08:00

191 lines
6.4 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"
"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"
// 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 {
// 不吞真实错误:与扫码登录 StartScanLogin 一致,把微信返回的
// errcode 与排查建议(IP 白名单 / 48001 未认证等)透出,便于定位;
// 同时保留「公众号内发送注册」兜底路径,会话仍有效可轮询核销。
notice := "请在公众号内直接发送「注册」获取验证码"
var apiErr *APIError
if errors.As(err, &apiErr) {
notice = "生成带参二维码失败(" + apiErr.Error() + ")。" + notice
if hint := apiErr.Explain(); hint != "" {
notice += ":" + hint
}
} else {
notice = "生成带参二维码失败:" + err.Error() + "。" + notice
}
return scene, "", notice, expireAt, nil
}
return scene, qrURL, "", expireAt, 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
}