feat(设备管理): 添加设备在线记录查询功能并优化设备列表排序
Build docker and publish / build (20.15.1) (push) Successful in 7m28s

添加FindLatestDeviceOnlineRecord接口用于查询设备最新在线记录
实现GetOnlineDeviceLoginTime方法获取设备登录时间
优化设备列表查询按最后活动时间排序
移除未使用的依赖项
This commit is contained in:
2025-11-27 23:24:48 -08:00
parent 236fa6c4e6
commit 2442831cd7
12 changed files with 210 additions and 130 deletions
+7 -7
View File
@@ -140,9 +140,9 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error())
}
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.Config.JwtAuth.MaxSessionsPerUser); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
}
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.SessionLimit()); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
}
// Store session id in redis
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil {
@@ -164,10 +164,10 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
}
loginStatus = true
return &types.LoginResponse{
Token: token,
Limit: l.svcCtx.Config.JwtAuth.MaxSessionsPerUser,
}, nil
return &types.LoginResponse{
Token: token,
Limit: l.svcCtx.SessionLimit(),
}, nil
}
func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest) (*user.User, error) {
@@ -78,11 +78,11 @@ func (l *OAuthLoginGetTokenLogic) OAuthLoginGetToken(req *types.OAuthLoginGetTok
return nil, err
}
loginStatus = true
return &types.LoginResponse{
Token: token,
Limit: l.svcCtx.Config.JwtAuth.MaxSessionsPerUser,
}, nil
loginStatus = true
return &types.LoginResponse{
Token: token,
Limit: l.svcCtx.SessionLimit(),
}, nil
}
func (l *OAuthLoginGetTokenLogic) google(req *types.OAuthLoginGetTokenRequest, requestID, ip, userAgent string) (*user.User, error) {
@@ -590,9 +590,9 @@ func (l *OAuthLoginGetTokenLogic) generateToken(userInfo *user.User, requestID s
return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err)
}
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.Config.JwtAuth.MaxSessionsPerUser); err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
}
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.SessionLimit()); err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
}
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil {
+7 -7
View File
@@ -156,16 +156,16 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error())
}
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.Config.JwtAuth.MaxSessionsPerUser); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
}
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.SessionLimit()); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
}
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error())
}
loginStatus = true
return &types.LoginResponse{
Token: token,
Limit: l.svcCtx.Config.JwtAuth.MaxSessionsPerUser,
}, nil
return &types.LoginResponse{
Token: token,
Limit: l.svcCtx.SessionLimit(),
}, nil
}
+7 -7
View File
@@ -111,16 +111,16 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error())
}
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.Config.JwtAuth.MaxSessionsPerUser); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
}
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.SessionLimit()); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
}
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error())
}
loginStatus = true
return &types.LoginResponse{
Token: token,
Limit: l.svcCtx.Config.JwtAuth.MaxSessionsPerUser,
}, nil
return &types.LoginResponse{
Token: token,
Limit: l.svcCtx.SessionLimit(),
}, nil
}
@@ -69,23 +69,23 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
// 检查邮箱是否已被其他用户绑定
existingMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email)
var emailUserId int64
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 邮箱不存在,不创建新用户,直接将邮箱认证绑定到当前设备用户
l.Infow(" 为当前设备做 邮箱绑定操作; 在 user_auth_methods 中添加记录", logger.Field("email", req.Email))
err = l.addAuthMethodForEmailUser(u.Id, req.Email)
if err != nil {
l.Errorw("添加邮箱用户认证方法失败", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "添加邮箱用户认证方法失败")
}
// 关键修复:为后续 token 生成与返回结果赋值绑定后的用户ID
emailUserId = u.Id
} else {
// 数据库查询错误
l.Errorw("查询邮箱绑定状态失败", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "查询邮箱绑定状态失败")
}
} else if existingMethod.Id != 0 {
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 邮箱不存在,不创建新用户,直接将邮箱认证绑定到当前设备用户
l.Infow(" 为当前设备做 邮箱绑定操作; 在 user_auth_methods 中添加记录", logger.Field("email", req.Email))
err = l.addAuthMethodForEmailUser(u.Id, req.Email)
if err != nil {
l.Errorw("添加邮箱用户认证方法失败", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "添加邮箱用户认证方法失败")
}
// 关键修复:为后续 token 生成与返回结果赋值绑定后的用户ID
emailUserId = u.Id
} else {
// 数据库查询错误
l.Errorw("查询邮箱绑定状态失败", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "查询邮箱绑定状态失败")
}
} else if existingMethod.Id != 0 {
// 邮箱已存在,使用现有的邮箱用户
emailUserId = existingMethod.UserId
l.Infow("邮箱已存在,将设备转移到现有邮箱用户",
@@ -1,14 +1,16 @@
package user
import (
"context"
"context"
"sort"
"time"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
)
type GetDeviceListLogic struct {
@@ -27,13 +29,39 @@ func NewGetDeviceListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get
}
func (l *GetDeviceListLogic) GetDeviceList() (resp *types.GetDeviceListResponse, err error) {
userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User)
list, count, err := l.svcCtx.UserModel.QueryDeviceList(l.ctx, userInfo.Id)
userRespList := make([]types.UserDevice, 0)
tool.DeepCopy(&userRespList, list)
resp = &types.GetDeviceListResponse{
Total: count,
List: userRespList,
}
return
userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User)
list, count, err := l.svcCtx.UserModel.QueryDeviceList(l.ctx, userInfo.Id)
if err != nil {
return nil, err
}
type item struct {
dev *user.Device
when time.Time
}
items := make([]item, 0, len(list))
for _, d := range list {
t, ok := l.svcCtx.DeviceManager.GetOnlineDeviceLoginTime(userInfo.Id, d.Identifier)
if !ok {
rec, recErr := l.svcCtx.UserModel.FindLatestDeviceOnlineRecord(l.ctx, userInfo.Id, d.Identifier)
if recErr == nil && rec != nil {
t = rec.OnlineTime
} else {
if d.UpdatedAt.After(d.CreatedAt) {
t = d.UpdatedAt
} else {
t = d.CreatedAt
}
}
}
items = append(items, item{dev: d, when: t})
}
sort.Slice(items, func(i, j int) bool { return items[i].when.After(items[j].when) })
userRespList := make([]types.UserDevice, 0, len(items))
for _, it := range items {
var ud types.UserDevice
tool.DeepCopy(&ud, it.dev)
userRespList = append(userRespList, ud)
}
resp = &types.GetDeviceListResponse{Total: count, List: userRespList}
return
}