This commit is contained in:
hero920103 committed 2026-11-03 14:55:26 +08:00
1 parent d16dbc69b4
commit fc8c3ee3ad
2 files changed
+370 -113

No files matched your search

+272 -18
View File
@@ -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 {