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

245 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 wechatmini
import (
"crypto/rand"
"errors"
"strconv"
"strings"
"time"
"github.com/beego/beego/v2/client/orm"
"github.com/beego/beego/v2/core/logs"
beego "github.com/beego/beego/v2/server/web"
"server/models"
)
// =============================================================
// 注册「账号注册」身份核验(小程序方案):
// 网页发起会话 → 展示小程序码(scene)→ 用户微信扫码进入小程序「注册确认」页 →
// 小程序端 wx.login 拿 code(可选手机号快速验证)调 confirm 上报 openid →
// 网页轮询到「已确认」后继续填写账号密码 → 第一步核验按 scene 核销取 openid,
// 注册完成后把该 openid 绑定到新身份(provider=wechat_mini),即可用小程序一键登录。
// =============================================================
const (
// DefaultRegisterTTL 注册会话有效期
DefaultRegisterTTL = 10 * time.Minute
// RegisterSceneLen 会话 scene 长度(微信限制 scene 不超过 32 字符)
RegisterSceneLen = 20
// registerSceneChars scene 字符集(只用小写字母与数字,避免扫码参数转义问题)
registerSceneChars = "abcdefghijklmnopqrstuvwxyz0123456789"
)
var (
ErrRegisterSceneInvalid = errors.New("注册会话不存在或已过期,请重新获取二维码")
ErrRegisterNotConfirmed = errors.New("请先用微信扫码并在小程序内完成确认")
ErrRegisterSceneUsed = errors.New("该二维码已完成核验,请重新获取")
)
// RegisterPagePath 小程序码指向的页面(app.conf:wechat_mini_register_page,默认 pages/register/confirm)
func RegisterPagePath() string {
page, _ := beego.AppConfig.String("wechat_mini_register_page")
page = strings.TrimPrefix(strings.TrimSpace(page), "/")
if page == "" {
return "pages/register/confirm"
}
return page
}
// RegisterEnvVersion 小程序码环境(app.conf:wechat_mini_register_env,默认 release;
// 未发布的小程序可填 trial 指向体验版、develop 指向开发版)
func RegisterEnvVersion() string {
env, _ := beego.AppConfig.String("wechat_mini_register_env")
env = strings.ToLower(strings.TrimSpace(env))
switch env {
case "trial", "develop":
return env
}
return "release"
}
// StartRegisterVerify 创建注册会话并生成小程序码(返回 data URL 可直接展示)。
// 小程序未配置 / 小程序码生成失败时返回 err,由上层回退公众号或短信通道。
func StartRegisterVerify(ttl time.Duration) (scene, qrURL string, expireAt time.Time, err error) {
cfg, err := LoadEnabledConfig()
if err != nil {
return "", "", time.Time{}, err
}
if ttl <= 0 {
ttl = DefaultRegisterTTL
}
scene = randomScene(RegisterSceneLen)
now := time.Now()
expireAt = now.Add(ttl)
row := &models.AuthMiniRegister{
Scene: scene,
AppID: cfg.AppID,
Status: models.MiniRegisterStatusWaiting,
ExpireAt: expireAt,
CreateTime: now,
UpdateTime: &now,
}
if _, err = models.Orm.Insert(row); err != nil {
return "", "", time.Time{}, err
}
qrURL, err = CreateUnlimitedQRCode(cfg, scene, RegisterPagePath(), RegisterEnvVersion())
if err != nil {
logs.Warning("[wechat-mini] 生成注册小程序码失败(页面 %s / 环境 %s):%v",
RegisterPagePath(), RegisterEnvVersion(), err)
return "", "", time.Time{}, err
}
return scene, qrURL, expireAt, nil
}
// GetRegisterStatus 查询会话是否已被小程序端确认(过期惰性标记)
func GetRegisterStatus(scene string) (confirmed bool, expireAt time.Time, err error) {
row, err := loadRegister(scene)
if err != nil {
return false, time.Time{}, err
}
expireAt = row.ExpireAt
if row.Status == models.MiniRegisterStatusWaiting && expireAt.Before(time.Now()) {
markRegisterExpired(row)
return false, expireAt, nil
}
return row.Status == models.MiniRegisterStatusConfirmed, expireAt, nil
}
// ConfirmRegister 小程序端确认注册:code → openid/unionid(可选手机号),CAS 标记已确认。
// appID 为空时取第一条启用的小程序配置;多小程序场景由小程序端传入自身 AppID。
func ConfirmRegister(scene, code, phoneCode, appID string) error {
scene = strings.TrimSpace(scene)
code = strings.TrimSpace(code)
if scene == "" || code == "" {
return errors.New("参数不完整")
}
cfg, err := loadConfigByAppID(strings.TrimSpace(appID))
if err != nil {
return err
}
session, err := Code2Session(cfg, code)
if err != nil {
logs.Warning("[wechat-mini] 注册确认 code 换取 openid 失败: %v", err)
return err
}
// 手机号快速验证为可选项:未开通 / code 失效时不阻断确认
mobile := ""
if strings.TrimSpace(phoneCode) != "" {
phone, perr := GetPhoneNumber(cfg, strings.TrimSpace(phoneCode))
if perr != nil {
logs.Warning("[wechat-mini] 注册确认获取手机号失败: %v", perr)
} else if phone != nil {
mobile = strings.TrimSpace(phone.PurePhoneNumber)
if mobile == "" {
mobile = strings.TrimSpace(phone.PhoneNumber)
}
}
}
row, err := loadRegister(scene)
if err != nil {
return err
}
if row.ExpireAt.Before(time.Now()) {
markRegisterExpired(row)
return ErrRegisterSceneInvalid
}
now := time.Now()
affected, err := models.Orm.QueryTable(new(models.AuthMiniRegister)).
Filter("id", row.ID).
Filter("status", models.MiniRegisterStatusWaiting).
Update(map[string]interface{}{
"openid": session.OpenID,
"unionid": session.UnionID,
"mobile": mobile,
"app_id": cfg.AppID,
"status": models.MiniRegisterStatusConfirmed,
"update_time": now,
})
if err != nil {
return err
}
if affected == 0 {
return errors.New("该二维码已确认或已失效,请重新获取")
}
return nil
}
// ConsumeRegister 核销会话并取出确认结果(一次性,供注册第一步核验使用)
func ConsumeRegister(scene string) (openid, unionid, mobile string, err error) {
row, err := loadRegister(scene)
if err != nil {
return "", "", "", err
}
if row.ExpireAt.Before(time.Now()) {
markRegisterExpired(row)
return "", "", "", ErrRegisterSceneInvalid
}
switch row.Status {
case models.MiniRegisterStatusWaiting:
return "", "", "", ErrRegisterNotConfirmed
case models.MiniRegisterStatusUsed:
return "", "", "", ErrRegisterSceneUsed
case models.MiniRegisterStatusExpired:
return "", "", "", ErrRegisterSceneInvalid
}
now := time.Now()
affected, err := models.Orm.QueryTable(new(models.AuthMiniRegister)).
Filter("id", row.ID).
Filter("status", models.MiniRegisterStatusConfirmed).
Update(map[string]interface{}{"status": models.MiniRegisterStatusUsed, "update_time": now})
if err != nil {
return "", "", "", err
}
if affected == 0 {
return "", "", "", ErrRegisterSceneUsed
}
return row.OpenID, row.UnionID, row.Mobile, nil
}
// loadRegister 按 scene 读取会话
func loadRegister(scene string) (*models.AuthMiniRegister, error) {
scene = strings.TrimSpace(scene)
if scene == "" {
return nil, ErrRegisterSceneInvalid
}
var row models.AuthMiniRegister
if err := models.Orm.QueryTable(new(models.AuthMiniRegister)).Filter("scene", scene).One(&row); err != nil {
if err == orm.ErrNoRows {
return nil, ErrRegisterSceneInvalid
}
return nil, err
}
return &row, nil
}
// markRegisterExpired 惰性标记过期
func markRegisterExpired(row *models.AuthMiniRegister) {
now := time.Now()
_, _ = models.Orm.QueryTable(new(models.AuthMiniRegister)).
Filter("id", row.ID).
Filter("status", models.MiniRegisterStatusWaiting).
Update(map[string]interface{}{"status": models.MiniRegisterStatusExpired, "update_time": now})
}
// loadConfigByAppID 按 AppID 选择小程序配置,缺省取第一条启用配置
func loadConfigByAppID(appID string) (*Config, error) {
if appID != "" {
return LoadEnabledAppByAppID(appID)
}
return LoadEnabledConfig()
}
func randomScene(n int) string {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
// 极低概率:退化为时间戳兜底,保证 scene 非空且合法
return "s" + strconv.FormatInt(time.Now().UnixNano(), 10)
}
for i := range b {
b[i] = registerSceneChars[int(b[i])%len(registerSceneChars)]
}
return string(b)
}