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 }