优化小程序
This commit is contained in:
1 parent
2e2b837a20
commit
727319a6eb
24 files changed
+1509
-200
No files matched your search
@@ -0,0 +1,244 @@
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user