// package system // @Link https://github.com/xiujiecn/xiu-admin // @Copyright Copyright (c) 2025 LiXiujie // @Author Lxj // @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE package system import ( "context" "errors" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/bcache" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/event" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/smsgateway" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility" "slices" "strings" "time" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" ) type sSysAuth struct { } // 手机验证码登录:验证码 Redis 键与有效期。 const ( smsLoginCodeKeyPrefix = "auth:sms:login:code:" smsLoginLimitKeyPrefix = "auth:sms:login:limit:" smsLoginAttemptKeyPrefix = "auth:sms:login:attempt:" smsLoginCodeTTLSeconds = 300 // 验证码有效期 5 分钟 smsLoginLimitTTLSeconds = 60 // 发送间隔 60 秒 smsLoginMaxAttempts = 5 // 每个验证码最多尝试次数 ) // smsLoginConsumeScript 原子校验并删除验证码:值匹配返回 1,否则返回 0。 const smsLoginConsumeScript = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end` func NewSysAuth() *sSysAuth { return &sSysAuth{} } func init() { service.RegisterSysAuth(NewSysAuth()) } func (s *sSysAuth) Login(ctx context.Context, param *model.LoginParams) (res *model.LoginUserOut, token string, err error) { ip := g.RequestFromCtx(ctx).GetClientIp() logininfor := &model.SysLogininforAddModel{ TenantId: param.TenantId, UserName: param.Username, ClientKey: "web", DeviceType: "web", Ipaddr: ip, LoginLocation: utility.GetCityByIp(ip), Browser: utility.GetBrowser(ctx), Os: utility.GetOs(ctx), Status: "0", Msg: "登录成功", LoginTime: gtime.Now(), } // 验证验证码 err = service.SysCaptcha().VerifyCaptcha(ctx, param.CaptchaID, param.CaptchaValue) if err != nil { logininfor.Status = "1" logininfor.Msg = err.Error() return nil, "", err } // 获取用户信息 user, err := service.SysUser().GetUserByUsernameAndPassword(ctx, param.TenantId, param.Username, param.Password) if err != nil { logininfor.Status = "1" logininfor.Msg = err.Error() return nil, "", err } userOut := &model.LoginUserOut{ ID: user.UserId, Username: user.UserName, NickName: user.NickName, Avatar: user.Avatar, TenantId: user.TenantId, DeptId: user.DeptId, } // 构建临时的上下文 identity := &model.Identity{ BaseClaims: model.BaseClaims{ ID: user.UserId, Username: user.UserName, NickName: user.NickName, TenantId: user.TenantId, DeptId: user.DeptId, Roles: []model.Role{}, LoginAt: time.Now().Unix(), }, } contextModel := new(model.Context) contextModel.User = identity ctx = contexts.Set(ctx, contextModel) // 生成token claims, token, err := s.GenerateToken(ctx, userOut) if err != nil { logininfor.Status = "1" logininfor.Msg = err.Error() return nil, "", err } if err = service.SysUser().UpdateLoginInfo(ctx, user.UserId, ip); err != nil { g.Log().Warningf(ctx, "sSysAuth.Login UpdateLoginInfo failed, userId: %d, ip: %s, error: %v", user.UserId, ip, err) } // 保存登录日志 go func() { id, err := service.SysLogininfor().AddLogininfor(ctx, logininfor) if err != nil { return } // 保存在线列表 service.SysUserOnline().Add(ctx, &model.SysUserOnlineAddModel{ OnlineId: id, TenantId: user.TenantId, Uuid: claims.UUID, UserName: user.UserName, ClientKey: "web", DeviceType: "web", Ipaddr: ip, LoginLocation: utility.GetCityByIp(ip), Browser: utility.GetBrowser(ctx), Os: utility.GetOs(ctx), Token: token, LoginTime: gtime.Now(), ExpireTime: gtime.NewFromTime(claims.ExpiresAt.Time), }) }() event.EventsInstance().Emit(ctx, consts.EventKeyUserLogin, user.UserId) return userOut, token, nil } func (s *sSysAuth) HasAccessCode(ctx context.Context, requiredCode string) (bool, error) { requiredCode = strings.TrimSpace(requiredCode) if requiredCode == "" { return false, nil } userId := contexts.GetUserId(ctx) if userId <= 0 { return false, nil } if contexts.IsSuperAdmin(ctx) && contexts.GetTenantId(ctx) == consts.DefaultSystemTenantCode { isAdmin, err := rolepolicy.CurrentSuperAdmin(ctx, userId) if err != nil || isAdmin { return isAdmin, err } } accessCodeList, _, err := s.GetUserAccessCodeList(ctx, userId) if err != nil { return false, err } for _, userCode := range accessCodeList { if sysAuthMatchAccessCode(strings.TrimSpace(userCode), requiredCode) { return true, nil } } return false, nil } func sysAuthMatchAccessCode(pattern, code string) bool { if pattern == code { return true } if strings.HasSuffix(pattern, "*") { prefix := pattern[:len(pattern)-1] return strings.HasPrefix(code, prefix) } return false } // 生成Token func (s *sSysAuth) GenerateToken(ctx context.Context, user *model.LoginUserOut) (claims *model.CustomClaims, token string, err error) { ets := g.Cfg().MustGet(ctx, "jwt.expiresTime", "7d").String() bts := g.Cfg().MustGet(ctx, "jwt.bufferTime", "1d").String() iss := g.Cfg().MustGet(ctx, "jwt.issuer", "IotLCM").String() sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "${JWT_SIGNING_KEY}").String() et, _ := utility.ParseDuration(ets) bt, _ := utility.ParseDuration(bts) g.Log().Infof(ctx, "sSysAuth.GenerateToken 生成Token: user:%+v, et:%s, bt:%s, iss:%s, sk:%s", user, et, bt, iss, sk) roles := make([]model.Role, 0) if len(user.RoleIds) > 0 { for _, roleId := range user.RoleIds { roles = append(roles, model.Role{ RoleId: roleId, DataScope: consts.SysRoleDataScopeAll, }) } } else { authorityIds, err := service.SysUser().GetUserRoleIds(ctx, user.ID) if err != nil { return nil, "", err } rl, _, err := service.SysRole().List(ctx, &model.SysRoleListParam{ PageInfo: request.PageInfo{ Page: 1, PageSize: 5000, }, RoleIds: authorityIds, }) if err != nil { return nil, "", err } for _, r := range rl { roles = append(roles, model.Role{ RoleId: r.RoleId, DataScope: r.DataScope, }) } } g.Log().Debugf(ctx, "sSysAuth.GenerateToken roles: %v", roles) uuidStr := user.UUID if uuidStr == "" { uuidStr = strings.ReplaceAll(uuid.New().String(), "-", "") } claims = &model.CustomClaims{ BaseClaims: model.BaseClaims{ UUID: uuidStr, ID: user.ID, Username: user.Username, NickName: user.NickName, TenantId: user.TenantId, DeptId: user.DeptId, Roles: roles, LoginAt: time.Now().Unix(), }, BufferTime: int64(bt / time.Second), RegisteredClaims: jwt.RegisteredClaims{ Audience: jwt.ClaimStrings{"IotLCM"}, // 受众 ExpiresAt: jwt.NewNumericDate(time.Now().Add(et)), // 过期时间 7天 配置文件 NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), // 签名生效时间 Issuer: iss, // 发行人 }, } t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token, err = t.SignedString([]byte(sk)) if err != nil { return nil, "", err } g.Log().Infof(ctx, "sSysAuth.GenerateToken token: %s, claims: %+v", token, claims) bcache.SetSysAuthToken(ctx, claims.BaseClaims.ID, claims.BaseClaims.UUID, token, et) return claims, token, nil } // 解析Token func (s *sSysAuth) ParseToken(ctx context.Context, token string) (claims *model.CustomClaims, err error) { sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "${JWT_SIGNING_KEY}").String() t, err := jwt.ParseWithClaims(token, &model.CustomClaims{}, func(token *jwt.Token) (interface{}, error) { return []byte(sk), nil }) if err != nil { return nil, err } if t.Claims.(*model.CustomClaims).ExpiresAt.Unix() < time.Now().Unix() { return nil, errors.New("token已过期") } baseClaims := t.Claims.(*model.CustomClaims).BaseClaims _, err = bcache.GetSysAuthToken(ctx, baseClaims.ID, baseClaims.UUID) if err != nil { g.Log().Errorf(ctx, "sSysAuth.ParseToken GetSysAuthToken failed, userId: %d, uuid: %s, error: %v", baseClaims.ID, baseClaims.UUID, err) return nil, err } return t.Claims.(*model.CustomClaims), nil } // 删除Token func (s *sSysAuth) DeleteToken(ctx context.Context, token string) (err error) { sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "${JWT_SIGNING_KEY}").String() t, err := jwt.ParseWithClaims(token, &model.CustomClaims{}, func(token *jwt.Token) (interface{}, error) { return []byte(sk), nil }) if err != nil { return err } timeout := t.Claims.(*model.CustomClaims).ExpiresAt.Unix() - time.Now().Unix() if timeout <= 0 { return nil } err = bcache.DelSysAuthToken(ctx, t.Claims.(*model.CustomClaims).BaseClaims.ID, t.Claims.(*model.CustomClaims).BaseClaims.UUID) if err != nil { return err } bcache.SetSysAuthTokenReject(ctx, t.Claims.(*model.CustomClaims).BaseClaims.ID, t.Claims.(*model.CustomClaims).BaseClaims.UUID, token, time.Duration(timeout)*time.Second) return nil } // 获取token func (s *sSysAuth) GetAccessToken(ctx context.Context) (token string, err error) { authorization := g.RequestFromCtx(ctx).Header.Get("Authorization") authParam := g.RequestFromCtx(ctx).Get("access_token").String() if authorization == "" && authParam == "" { return "", gerror.NewCode(gcode.CodeNotImplemented) } token = strings.TrimPrefix(authorization, "Bearer ") if token == "" { token = authParam } return token, nil } // 根据Token获取当前登录用户信息 func (s *sSysAuth) GetCurrentUser(ctx context.Context) (claims *model.CustomClaims, err error) { // 获取token token, err := s.GetAccessToken(ctx) if err != nil { return nil, err } // 解析token claims, err = s.ParseToken(ctx, token) if err != nil { g.Log().Warningf(ctx, "sSysAuth.GetCurrentUser ParseToken failed, token: %s, error: %v", token, err) return nil, err } return claims, nil } // 获取用户权限码 func (s *sSysAuth) GetUserAccessCodeList(ctx context.Context, userId int64) (accessCodeList []string, menuRoleAccessCodeList []string, err error) { accessCodeList = make([]string, 0) menuRoleAccessCodeList = make([]string, 0) roleDataScopeMap := make(map[int64]string) template, templateErr := rolepolicy.Template(ctx) if templateErr != nil { return nil, nil, templateErr } managed := template != nil && template.TenantId == contexts.GetTenantId(ctx) && template.DeptId != contexts.GetDeptId(ctx) // 角色权限码 roleIds, err := service.SysUser().GetUserRoleIds(ctx, userId) if err != nil { return nil, nil, err } var effectiveRoles []*entity.SysRole if len(roleIds) > 0 { if err := dao.SysRole.Ctx(ctx).Where("tenant_id", contexts.GetTenantId(ctx)).WhereIn("role_id", roleIds).Where("status", "0").Scan(&effectiveRoles); err != nil { return nil, nil, err } } roleIds = nil for _, role := range effectiveRoles { roleIds = append(roleIds, role.RoleId) } for _, role := range effectiveRoles { accessCodeList = append(accessCodeList, consts.SysCheckPermissionRolePrefix+role.RoleKey) roleDataScopeMap[role.RoleId] = role.DataScope } if user := contexts.GetUser(ctx); user != nil && user.ID == userId { user.Roles = make([]model.Role, 0, len(effectiveRoles)) for _, role := range effectiveRoles { scope := role.DataScope if managed && (scope == consts.SysRoleDataScopeAll || scope == consts.SysRoleDataScopeCustom) { scope = consts.SysRoleDataScopeDeptAndBelow } if scope == "" { scope = consts.SysRoleDataScopePersonal } user.Roles = append(user.Roles, model.Role{RoleId: role.RoleId, DataScope: scope}) } } g.Log().Infof(ctx, "sSysAuth.GetUserAccessCodeList roleIds: %v", roleIds) // 菜单权限码 // 获取角色菜单列表 rmList, err := service.SysRole().GetRoleListMenu(ctx, roleIds) if err != nil { return nil, nil, err } menuIds := make([]int64, 0) for _, menu := range rmList { menuIds = append(menuIds, menu.MenuId) } if !contexts.IsSuperAdmin(ctx) { menuIds, err = rolepolicy.RestrictMenus(ctx, contexts.GetTenantId(ctx), contexts.GetDeptId(ctx), menuIds) if err != nil { return nil, nil, err } } g.Log().Infof(ctx, "sSysAuth.GetUserAccessCodeList menuIds: %v", menuIds) if len(menuIds) > 0 { // 获取用户角色菜单 menuList, _, err := service.SysMenu().List(ctx, &model.SysMenuListParam{ MenuIds: menuIds, }) if err != nil { return nil, nil, err } g.Log().Infof(ctx, "sSysAuth.GetUserAccessCodeList menuList: %v", menuList) for _, m := range menuList { menuAccessCodes := normalizeMenuAccessCodes(m.Perms) if len(menuAccessCodes) == 0 { continue } accessCodeList = append(accessCodeList, menuAccessCodes...) for _, r := range rmList { if r.MenuId == m.MenuId { roleDS := roleDataScopeMap[r.RoleId] if r.DataScope != "" && r.DataScope != consts.SysRoleMenuDataScopeRole { roleDS = r.DataScope } if managed && (roleDS == consts.SysRoleDataScopeAll || roleDS == consts.SysRoleDataScopeCustom) { roleDS = consts.SysRoleDataScopeDeptAndBelow } if len(roleDS) < 1 { roleDS = consts.SysRoleDataScopePersonal } for _, accessCode := range menuAccessCodes { menuRoleAccessCodeList = append(menuRoleAccessCodeList, accessCode+"|"+gconv.String(r.RoleId)+"|"+roleDS) } } } } } g.Log().Infof(ctx, "sSysAuth.GetUserAccessCodeList accessCodeList: %v", accessCodeList) // 获取用户岗位信息 postIds, err := service.SysUser().GetUserPostIds(ctx, userId) if err != nil { return nil, nil, err } if len(postIds) > 0 { pl, _, err := service.SysPost().List(ctx, &model.SysPostListParam{ PageInfo: request.PageInfo{ Page: 1, PageSize: 5000, }, PostIds: postIds, }) if err != nil { return nil, nil, err } for _, p := range pl { accessCodeList = append(accessCodeList, consts.SysCheckPermissionPostPrefix+p.PostCode) } } // 获取用户信息 user, err := service.SysUser().GetUserById(ctx, userId) if err != nil { return nil, nil, err } accessCodeList = append(accessCodeList, consts.SysCheckPermissionUserPrefix+user.UserName) accessCodeList = append(accessCodeList, consts.SysCheckPermissionCurrentUser) accessCodeList = slices.Compact(accessCodeList) return accessCodeList, menuRoleAccessCodeList, nil } // 根据openId登录 func (s *sSysAuth) LoginByOpenId(ctx context.Context, social *model.SysSocialListModel) (res *model.LoginUserOut, token string, err error) { ip := g.RequestFromCtx(ctx).GetClientIp() logininfor := &model.SysLogininforAddModel{ TenantId: social.User.TenantId, UserName: social.User.UserName, ClientKey: "web", DeviceType: "web", Ipaddr: ip, LoginLocation: utility.GetCityByIp(ip), Browser: utility.GetBrowser(ctx), Os: utility.GetOs(ctx), Status: "0", Msg: "登录成功", LoginTime: gtime.Now(), } user, err := service.SysUser().GetUserByUsername(ctx, social.User.UserName, social.User.TenantId) if err != nil { return nil, "用户不存在", err } userOut := &model.LoginUserOut{ ID: user.UserId, Username: user.UserName, NickName: user.NickName, Avatar: user.Avatar, TenantId: user.TenantId, DeptId: user.DeptId, } // 构建临时的上下文 identity := &model.Identity{ BaseClaims: model.BaseClaims{ ID: user.UserId, Username: user.UserName, NickName: user.NickName, TenantId: user.TenantId, DeptId: user.DeptId, Roles: []model.Role{}, LoginAt: time.Now().Unix(), }, } contextModel := new(model.Context) contextModel.User = identity ctx = contexts.Set(ctx, contextModel) // 生成token claims, token, err := s.GenerateToken(ctx, userOut) if err != nil { logininfor.Status = "1" logininfor.Msg = err.Error() return nil, "", err } if err = service.SysUser().UpdateLoginInfo(ctx, user.UserId, ip); err != nil { g.Log().Warningf(ctx, "sSysAuth.LoginByOpenId UpdateLoginInfo failed, userId: %d, ip: %s, error: %v", user.UserId, ip, err) } // 保存登录日志 go func() { id, err := service.SysLogininfor().AddLogininfor(ctx, logininfor) if err != nil { return } // 保存在线列表 service.SysUserOnline().Add(ctx, &model.SysUserOnlineAddModel{ OnlineId: id, TenantId: user.TenantId, Uuid: claims.UUID, UserName: user.UserName, ClientKey: "web", DeviceType: "web", Ipaddr: ip, LoginLocation: utility.GetCityByIp(ip), Browser: utility.GetBrowser(ctx), Os: utility.GetOs(ctx), Token: token, LoginTime: gtime.Now(), ExpireTime: gtime.NewFromTime(claims.ExpiresAt.Time), }) }() event.EventsInstance().Emit(ctx, consts.EventKeyUserLogin, user.UserId) return userOut, token, nil } // SendSmsLoginCode 发送手机验证码登录短信。 // 验证码 5 分钟有效,60 秒内不允许重复发送(SET NX EX 原子占用,防并发绕过)。 // 本接口未鉴权,网关不可用时必须 fail closed:不落库验证码、不打印验证码。 func (s *sSysAuth) SendSmsLoginCode(ctx context.Context, phone string) (err error) { phone = strings.TrimSpace(phone) if phone == "" { return gerror.New("手机号不能为空") } // 原子占用发送资格:NX 抢占失败说明间隔内已发送过。 limitKey := smsLoginLimitKeyPrefix + phone acquired, err := g.Redis().Do(ctx, "SET", limitKey, "1", "EX", smsLoginLimitTTLSeconds, "NX") if err != nil { return gerror.Wrap(err, "校验发送频率失败,请稍后重试") } if acquired.IsNil() { return gerror.New("验证码发送过于频繁,请稍后再试") } code, err := GenerateMfaCode(ctx) if err != nil { return err } codeKey := smsLoginCodeKeyPrefix + phone if _, err = g.Redis().Do(ctx, "SETEX", codeKey, smsLoginCodeTTLSeconds, code); err != nil { return gerror.Wrap(err, "保存验证码失败,请稍后重试") } sent, err := smsgateway.Send(ctx, smsgateway.Request{ Scene: smsgateway.SceneLogin, PhoneNumber: phone, MsgContent: code, }) if err != nil { _, _ = g.Redis().Del(ctx, codeKey) return gerror.Wrap(err, "短信发送失败,请稍后重试") } if !sent { _, _ = g.Redis().Del(ctx, codeKey) return gerror.New("短信网关未启用,无法发送验证码") } return nil } // smsLoginCheckUserStatus 登录前的账号状态校验,返回用户可读错误。 func smsLoginCheckUserStatus(user *entity.SysUser) error { switch user.Status { case consts.SysUserStatusDisable: return gerror.NewCode(consts.CodeUserDisabled, "账号已禁用") case consts.SysUserStatusLocked: return gerror.NewCode(consts.CodeUserLocked, "账号已锁定") case consts.SysUserStatusExpired: return gerror.NewCode(consts.CodeUserExpired, "账号已过期") case consts.SysUserStatusDeleted: return gerror.NewCode(consts.CodeUserDeleted, "账号已删除") } return nil } // LoginByPhoneCode 手机验证码登录:校验验证码后按手机号(跨租户)定位账号并签发 token。 // 同一手机号命中多个账号时拒绝登录,引导使用账号密码登录(账号密码可结合租户选择)。 // 验证码通过 Lua compare-and-del 原子校验并消费,防并发重放;每手机号限 5 次尝试。 func (s *sSysAuth) LoginByPhoneCode(ctx context.Context, phone string, code string) (res *model.LoginUserOut, token string, err error) { phone = strings.TrimSpace(phone) code = strings.TrimSpace(code) ip := g.RequestFromCtx(ctx).GetClientIp() codeKey := smsLoginCodeKeyPrefix + phone attemptKey := smsLoginAttemptKeyPrefix + phone // 尝试次数限制:防暴力枚举;TTL 与验证码对齐,成功登录后清除。 attempts, err := g.Redis().Do(ctx, "INCR", attemptKey) if err != nil { return nil, "", gerror.Wrap(err, "校验登录尝试失败,请稍后重试") } if attempts.Int64() == 1 { _, _ = g.Redis().Do(ctx, "EXPIRE", attemptKey, smsLoginCodeTTLSeconds) } if attempts.Int64() > smsLoginMaxAttempts { return nil, "", gerror.New("尝试次数过多,请重新获取验证码") } // 原子校验并消费验证码(compare-and-delete),消费成功才继续登录。 consumed, err := g.Redis().Do(ctx, "EVAL", smsLoginConsumeScript, 1, codeKey, code) if err != nil { return nil, "", gerror.Wrap(err, "校验验证码失败,请稍后重试") } if consumed.Int64() != 1 { return nil, "", gerror.New("验证码错误或已过期") } _, _ = g.Redis().Del(ctx, attemptKey) users, err := service.SysUser().ListUsersByPhone(ctx, phone) if err != nil { return nil, "", err } if len(users) == 0 { return nil, "", gerror.NewCode(consts.CodeUserNotFound, "账号不存在") } if len(users) > 1 { return nil, "", gerror.New("该手机号绑定了多个账号,请使用账号密码登录") } user := users[0] if err = smsLoginCheckUserStatus(user); err != nil { return nil, "", err } logininfor := &model.SysLogininforAddModel{ TenantId: user.TenantId, UserName: user.UserName, ClientKey: "web", DeviceType: "web", Ipaddr: ip, LoginLocation: utility.GetCityByIp(ip), Browser: utility.GetBrowser(ctx), Os: utility.GetOs(ctx), Status: "0", Msg: "登录成功", LoginTime: gtime.Now(), } userOut := &model.LoginUserOut{ ID: user.UserId, Username: user.UserName, NickName: user.NickName, Avatar: user.Avatar, TenantId: user.TenantId, DeptId: user.DeptId, } // 构建临时的上下文 identity := &model.Identity{ BaseClaims: model.BaseClaims{ ID: user.UserId, Username: user.UserName, NickName: user.NickName, TenantId: user.TenantId, DeptId: user.DeptId, Roles: []model.Role{}, LoginAt: time.Now().Unix(), }, } contextModel := new(model.Context) contextModel.User = identity ctx = contexts.Set(ctx, contextModel) // 生成token claims, token, err := s.GenerateToken(ctx, userOut) if err != nil { logininfor.Status = "1" logininfor.Msg = err.Error() return nil, "", err } if err = service.SysUser().UpdateLoginInfo(ctx, user.UserId, ip); err != nil { g.Log().Warningf(ctx, "sSysAuth.LoginByPhoneCode UpdateLoginInfo failed, userId: %d, ip: %s, error: %v", user.UserId, ip, err) } // 保存登录日志 go func() { id, err := service.SysLogininfor().AddLogininfor(ctx, logininfor) if err != nil { return } // 保存在线列表 service.SysUserOnline().Add(ctx, &model.SysUserOnlineAddModel{ OnlineId: id, TenantId: user.TenantId, Uuid: claims.UUID, UserName: user.UserName, ClientKey: "web", DeviceType: "web", Ipaddr: ip, LoginLocation: utility.GetCityByIp(ip), Browser: utility.GetBrowser(ctx), Os: utility.GetOs(ctx), Token: token, LoginTime: gtime.Now(), ExpireTime: gtime.NewFromTime(claims.ExpiresAt.Time), }) }() event.EventsInstance().Emit(ctx, consts.EventKeyUserLogin, user.UserId) return userOut, token, nil }