From fc8c3ee3ad182b7e72633efd89fa0b49a495cd45 Mon Sep 17 00:00:00 2001 From: lzq <357099073@qq.com> Date: Tue, 3 Nov 2026 14:55:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/controllers/guest.go | 290 ++++++++++++++++++++++++++++++++++++--- uniapp/pages/nav/nav.vue | 193 +++++++++++++------------- 2 files changed, 370 insertions(+), 113 deletions(-) diff --git a/go/controllers/guest.go b/go/controllers/guest.go index 8e987ee..3121017 100644 --- a/go/controllers/guest.go +++ b/go/controllers/guest.go @@ -21,6 +21,7 @@ import ( "server/models" "server/pkg/identity" "server/pkg/jwtutil" + authsvc "server/services/auth" "server/services/gueststore" "server/services/wechatmini" @@ -83,12 +84,11 @@ func (c *GuestController) MiniLogin() { return } - // openid 仅作溯源,拿不到不阻断游客签发 + // openid 仅作溯源与身份识别(已绑定过正式身份时按 openid 认人),拿不到不阻断游客签发 var openID, unionID string - if req.Code != "" { - if session, serr := wechatmini.Code2Session(cfg, req.Code); serr == nil && session != nil { - openID, unionID = session.OpenID, session.UnionID - } + session := guestMiniSession(cfg, req.Code) + if session != nil { + openID, unionID = session.OpenID, session.UnionID } if strings.TrimSpace(req.PhoneCode) == "" { @@ -114,6 +114,12 @@ func (c *GuestController) MiniLogin() { return } + // 手机号已注册云泽账号(或该微信已绑定正式身份):直接签发正式令牌按登录处理。 + // (与 /auth/wechat/miniapp/login 同一套识别与签发逻辑) + if c.tryUserLogin(mobile, session) { + return + } + guest, err := gueststore.Upsert(mobile, openID, unionID) if err != nil { logs.Error("[guest] upsert 失败: %v", err) @@ -143,6 +149,258 @@ func (c *GuestController) MiniLogin() { }) } +// identityByPhone 按微信已验证手机号定位认证中心身份。 +// 租户账号手机号可能落在三处,逐层兜底(先查认证中心,查不到再回退业务表): +// 1. yz_auth_identity.mobile/account/email(FindIdentityByAccount) +// 2. yz_auth_tenant_user.phone → identity_id +// 3. yz_system_tenant_user.phone → uid(全量迁移后 uid == identity_id) +func identityByPhone(mobile string) *models.AuthIdentity { + if ai, err := authsvc.FindIdentityByAccount(mobile); err == nil && ai != nil { + return ai + } + var identityID uint64 + var tu models.AuthTenantUser + if err := models.Orm.QueryTable(new(models.AuthTenantUser)). + Filter("phone", mobile).Filter("delete_time__isnull", true). + OrderBy("-is_default", "id").One(&tu); err == nil { + identityID = tu.IdentityID + } else { + var su models.SystemTenantUser + if err2 := models.Orm.QueryTable(new(models.SystemTenantUser)). + Filter("phone", mobile).Filter("delete_time__isnull", true). + OrderBy("-is_default", "id").One(&su); err2 == nil { + identityID = su.Uid + } + } + if identityID == 0 { + return nil + } + ai := &models.AuthIdentity{} + if err := models.Orm.QueryTable(new(models.AuthIdentity)). + Filter("id", identityID).Filter("delete_time__isnull", true).One(ai); err != nil { + return nil + } + return ai +} + +// phoneOfIdentity 取身份手机号:identity.mobile 为空时回退租户绑定/业务用户表。 +func phoneOfIdentity(identityID uint64) string { + var tus []models.AuthTenantUser + if _, err := models.Orm.QueryTable(new(models.AuthTenantUser)). + Filter("identity_id", identityID).Filter("delete_time__isnull", true). + OrderBy("-is_default", "id").All(&tus); err == nil { + for _, tu := range tus { + if tu.Phone != nil { + if p := strings.TrimSpace(*tu.Phone); p != "" { + return p + } + } + } + } + var sus []models.SystemTenantUser + if _, err := models.Orm.QueryTable(new(models.SystemTenantUser)). + Filter("uid", identityID).Filter("delete_time__isnull", true). + OrderBy("-is_default", "id").All(&sus); err == nil { + for _, su := range sus { + if su.Phone != nil { + if p := strings.TrimSpace(*su.Phone); p != "" { + return p + } + } + } + } + return "" +} + +// guestMiniSession 拿 wx.login code 换 openid/unionid;失败不阻断(返回 nil)。 +func guestMiniSession(cfg *wechatmini.Config, code string) *wechatmini.Session { + if strings.TrimSpace(code) == "" { + return nil + } + session, err := wechatmini.Code2Session(cfg, code) + if err != nil { + logs.Warn("[guest] code 换取 openid 失败: %v", err) + return nil + } + return session +} + +// identityByWechat 按 openid/unionid 查已绑定的正式身份(登录页静默 bind 的落点)。 +// 与小程序一键登录 resolveIdentity 同源:手机号查不到时的第二级认人方式。 +func identityByWechat(openID, unionID string) *models.AuthIdentity { + var bind models.AuthIdentityThird + qs := models.Orm.QueryTable(new(models.AuthIdentityThird)).Filter("provider", "wechat_mini") + found := false + if openID != "" { + if err := qs.Filter("open_id", openID).One(&bind); err == nil { + found = true + } + } + if !found && unionID != "" { + if err := models.Orm.QueryTable(new(models.AuthIdentityThird)). + Filter("provider", "wechat_mini").Filter("union_id", unionID).One(&bind); err == nil { + found = true + } + } + if !found { + return nil + } + ai := &models.AuthIdentity{} + if err := models.Orm.QueryTable(new(models.AuthIdentity)). + Filter("id", bind.IdentityID).Filter("delete_time__isnull", true).One(ai); err != nil { + return nil + } + return ai +} + +// bindWechatMini 把当前小程序 openid 绑到该身份。手机号已经微信验证, +// 属于强证据,允许覆盖旧绑定(与 controllers/auth syncBinding 的 verified 分支一致)。 +func bindWechatMini(session *wechatmini.Session, identityID uint64) { + var bind models.AuthIdentityThird + qs := models.Orm.QueryTable(new(models.AuthIdentityThird)).Filter("provider", "wechat_mini") + if err := qs.Filter("open_id", session.OpenID).One(&bind); err == nil { + if bind.IdentityID == identityID { + return + } + _, _ = models.Orm.QueryTable(new(models.AuthIdentityThird)).Filter("id", bind.ID).Delete() + } + item := &models.AuthIdentityThird{IdentityID: identityID, Provider: "wechat_mini", OpenID: session.OpenID} + if session.UnionID != "" { + uid := session.UnionID + item.UnionID = &uid + } + _, _ = models.Orm.Insert(item) +} + +// tryUserLogin 一键授权时的正式身份识别:手机号命中启用账号且该账号有企业, +// 则签发正式令牌并返回 type=user(对齐小程序一键登录:单企业直接进入, +// 多企业返回待选令牌由企业选择接口完成切换)。已处理返回 true。 +// 未命中 / 账号无企业 / 签发失败 → 返回 false,回落到游客签发流程。 +func (c *GuestController) tryUserLogin(mobile string, session *wechatmini.Session) bool { + var authIdentity *models.AuthIdentity + if session != nil { + authIdentity = identityByWechat(session.OpenID, session.UnionID) + } + if authIdentity == nil { + authIdentity = identityByPhone(mobile) + } + if authIdentity == nil { + return false + } + // 手机号已通过微信验证:把 openid 补绑/改绑到该身份(下次无手机号也能认人) + if session != nil && session.OpenID != "" { + bindWechatMini(session, authIdentity.ID) + } + if authIdentity.Status != models.AuthIdentityStatusEnabled { + c.gReply(403, "账号已被禁用或锁定,无法使用", nil) + return true + } + tenants, lerr := authsvc.ListTenantOptions(authIdentity.ID) + if lerr != nil || len(tenants) == 0 { + return false // 账号未加入任何企业:按游客模式继续使用 + } + + needChoose := len(tenants) > 1 + tid := tenants[0].Tid + accessTTL := 0 + if needChoose { + tid = authsvc.PendingTenantID + accessTTL = 600 // 待选状态令牌只够用户完成企业选择 + } + + username := "微信用户" + if authIdentity.Nickname != nil && strings.TrimSpace(*authIdentity.Nickname) != "" { + username = strings.TrimSpace(*authIdentity.Nickname) + } else if authIdentity.Mobile != nil && *authIdentity.Mobile != "" { + username = *authIdentity.Mobile + } + + sess, serr := authsvc.CreateSession(authsvc.SessionInfo{ + IdentityID: authIdentity.ID, + Tid: tid, + ClientID: "yz-miniapp", + IP: c.Ctx.Input.IP(), + UserAgent: c.Ctx.Request.UserAgent(), + LoginType: "wechat_mini", + Amr: "wechat_mini", + }) + if serr != nil { + logs.Error("[guest] 正式登录创建会话失败: %v", serr) + return false + } + tokens, terr := authsvc.IssueTokens(authsvc.TokenIssue{ + IdentityID: authIdentity.ID, + Tid: tid, + ClientID: "yz-miniapp", + Sid: sess.Sid, + Username: username, + UserType: "app", + Amr: "wechat_mini", + AccessTTL: accessTTL, + }) + if terr != nil { + logs.Error("[guest] 正式登录签发令牌失败: %v", terr) + return false + } + + identityIDCopy := authIdentity.ID + tidCopy := tid + amr := "wechat_mini" + _, _ = models.Orm.Insert(&models.AuthLoginLog{ + Tid: &tidCopy, + IdentityID: &identityIDCopy, + UserName: username, + ClientID: "yz-miniapp", + LoginType: "wechat_mini", + Amr: &amr, + Status: 1, + Message: "小程序手机号一键登录成功(免登录入口识别为正式账号)", + IP: c.Ctx.Input.IP(), + UserAgent: c.Ctx.Request.UserAgent(), + }) + + c.gReply(200, "success", map[string]interface{}{ + "type": "user", + "tokens": tokens, + "tenants": tenants, + "need_choose_tenant": needChoose, + }) + return true +} + +// findGuestForIdentity 找回当前身份对应的未合并游客记录: +// 优先按手机号,查不到再按该身份已绑定的微信 openid/unionid +// (微信验证号可能与账号注册号不一致,openid 是另一条可靠对应关系)。 +func findGuestForIdentity(identityID uint64, mobile string) (models.GuestUser, bool) { + var guest models.GuestUser + if mobile != "" { + err := models.Orm.QueryTable(new(models.GuestUser)). + Filter("phone", mobile).Filter("merged_uid", 0).One(&guest) + if err == nil { + return guest, true + } + } + var binds []models.AuthIdentityThird + if _, err := models.Orm.QueryTable(new(models.AuthIdentityThird)). + Filter("identity_id", identityID).Filter("provider", "wechat_mini").All(&binds); err == nil { + for _, b := range binds { + if b.OpenID != "" { + if err := models.Orm.QueryTable(new(models.GuestUser)). + Filter("openid", b.OpenID).Filter("merged_uid", 0).One(&guest); err == nil { + return guest, true + } + } + if b.UnionID != nil && *b.UnionID != "" { + if err := models.Orm.QueryTable(new(models.GuestUser)). + Filter("unionid", *b.UnionID).Filter("merged_uid", 0).One(&guest); err == nil { + return guest, true + } + } + } + } + return guest, false +} + // Merge POST /api/guest/merge(携带正式令牌) // 按当前身份手机号找回游客记录,把 yz_guest_data 各模块数据并入正式账号。 func (c *GuestController) Merge() { @@ -161,24 +419,20 @@ func (c *GuestController) Merge() { c.gReply(500, "查询身份失败", nil) return } - if ai.Mobile == nil || strings.TrimSpace(*ai.Mobile) == "" { - c.gReply(200, "无可合并数据", map[string]interface{}{"merged": false}) - return + // 回查手机号用于定位游客记录:identity.mobile 为空时回退租户绑定/业务用户表 + var mobile string + if ai.Mobile != nil { + mobile = strings.TrimSpace(*ai.Mobile) + } + if mobile == "" { + mobile = phoneOfIdentity(ai.ID) } - var guest models.GuestUser - err = models.Orm.QueryTable(new(models.GuestUser)). - Filter("phone", strings.TrimSpace(*ai.Mobile)). - Filter("merged_uid", 0). - One(&guest) - if err == orm.ErrNoRows { + guest, found := findGuestForIdentity(ai.ID, mobile) + if !found { c.gReply(200, "无可合并数据", map[string]interface{}{"merged": false}) return } - if err != nil { - c.gReply(500, "查询游客记录失败", nil) - return - } counts, merr := mergeGuestToUser(&guest, uint64(claims.UserID), claims.TenantId, claims.Username) if merr != nil { diff --git a/uniapp/pages/nav/nav.vue b/uniapp/pages/nav/nav.vue index 5169f48..8937650 100644 --- a/uniapp/pages/nav/nav.vue +++ b/uniapp/pages/nav/nav.vue @@ -56,16 +56,11 @@ - + - - 开启免登录保存 - 授权手机号后,记事本、日程提醒、密码库等免登录功能的数据即可保存;正式登录后会自动并入账号,不会丢失。 - - 暂不授权(使用将无法保存数据) - +