From 2442831cd7fec8c2950f0bcfb1a7df2adb4d88ad Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Thu, 27 Nov 2025 23:24:48 -0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=AE=BE=E5=A4=87=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AE=BE=E5=A4=87=E5=9C=A8=E7=BA=BF=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E6=9F=A5=E8=AF=A2=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=AE=BE=E5=A4=87=E5=88=97=E8=A1=A8=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加FindLatestDeviceOnlineRecord接口用于查询设备最新在线记录 实现GetOnlineDeviceLoginTime方法获取设备登录时间 优化设备列表查询按最后活动时间排序 移除未使用的依赖项 --- go.mod | 3 - go.sum | 10 -- internal/logic/auth/deviceLoginLogic.go | 14 +- .../auth/oauth/oAuthLoginGetTokenLogic.go | 16 +- internal/logic/auth/telephoneLoginLogic.go | 14 +- internal/logic/auth/userLoginLogic.go | 14 +- .../user/bindEmailWithVerificationLogic.go | 34 ++--- .../logic/public/user/getDeviceListLogic.go | 60 ++++++-- internal/model/user/device.go | 15 +- internal/model/user/model.go | 2 + internal/svc/serviceContext.go | 143 +++++++++++------- pkg/device/device.go | 15 ++ 12 files changed, 210 insertions(+), 130 deletions(-) diff --git a/go.mod b/go.mod index 248185a..2cd7114 100644 --- a/go.mod +++ b/go.mod @@ -61,7 +61,6 @@ require ( github.com/goccy/go-json v0.10.4 github.com/golang-migrate/migrate/v4 v4.18.2 github.com/spaolacci/murmur3 v1.1.0 - github.com/zeromicro/go-zero v1.9.2 google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.36.5 ) @@ -134,7 +133,6 @@ require ( go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/atomic v1.10.0 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.13.0 // indirect golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect @@ -145,5 +143,4 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 7990e1d..609f0cf 100644 --- a/go.sum +++ b/go.sum @@ -290,8 +290,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE= @@ -360,8 +358,6 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -github.com/zeromicro/go-zero v1.9.2 h1:ZXOXBIcazZ1pWAMiHyVnDQ3Sxwy7DYPzjE89Qtj9vqM= -github.com/zeromicro/go-zero v1.9.2/go.mod h1:k8YBMEFZKjTd4q/qO5RCW+zDgUlNyAs5vue3P4/Kmn0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= @@ -388,8 +384,6 @@ go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeX go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= @@ -543,8 +537,6 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -564,6 +556,4 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/internal/logic/auth/deviceLoginLogic.go b/internal/logic/auth/deviceLoginLogic.go index b2909ed..895dbcf 100644 --- a/internal/logic/auth/deviceLoginLogic.go +++ b/internal/logic/auth/deviceLoginLogic.go @@ -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) { diff --git a/internal/logic/auth/oauth/oAuthLoginGetTokenLogic.go b/internal/logic/auth/oauth/oAuthLoginGetTokenLogic.go index da7bd6c..3341480 100644 --- a/internal/logic/auth/oauth/oAuthLoginGetTokenLogic.go +++ b/internal/logic/auth/oauth/oAuthLoginGetTokenLogic.go @@ -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 { diff --git a/internal/logic/auth/telephoneLoginLogic.go b/internal/logic/auth/telephoneLoginLogic.go index 5af5364..b3169ec 100644 --- a/internal/logic/auth/telephoneLoginLogic.go +++ b/internal/logic/auth/telephoneLoginLogic.go @@ -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 } diff --git a/internal/logic/auth/userLoginLogic.go b/internal/logic/auth/userLoginLogic.go index fee6216..e0b8dad 100644 --- a/internal/logic/auth/userLoginLogic.go +++ b/internal/logic/auth/userLoginLogic.go @@ -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 } diff --git a/internal/logic/public/user/bindEmailWithVerificationLogic.go b/internal/logic/public/user/bindEmailWithVerificationLogic.go index 97b9a2b..91f5446 100644 --- a/internal/logic/public/user/bindEmailWithVerificationLogic.go +++ b/internal/logic/public/user/bindEmailWithVerificationLogic.go @@ -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("邮箱已存在,将设备转移到现有邮箱用户", diff --git a/internal/logic/public/user/getDeviceListLogic.go b/internal/logic/public/user/getDeviceListLogic.go index 76722d5..12b4432 100644 --- a/internal/logic/public/user/getDeviceListLogic.go +++ b/internal/logic/public/user/getDeviceListLogic.go @@ -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 } diff --git a/internal/model/user/device.go b/internal/model/user/device.go index 14168ce..8280d4a 100644 --- a/internal/model/user/device.go +++ b/internal/model/user/device.go @@ -40,7 +40,7 @@ func (m *customUserModel) QueryDevicePageList(ctx context.Context, userId, subsc var list []*Device var total int64 err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Device{}).Where("`user_id` = ? and `subscribe_id` = ?", userId, subscribeId).Count(&total).Limit(size).Offset((page - 1) * size).Find(&list).Error + return conn.Model(&Device{}).Where("`user_id` = ? and `subscribe_id` = ?", userId, subscribeId).Count(&total).Order("created_at DESC").Limit(size).Offset((page - 1) * size).Find(&list).Error }) return list, total, err } @@ -50,7 +50,7 @@ func (m *customUserModel) QueryDeviceList(ctx context.Context, userId int64) ([] var list []*Device var total int64 err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Device{}).Where("`user_id` = ?", userId).Count(&total).Find(&list).Error + return conn.Model(&Device{}).Where("`user_id` = ?", userId).Count(&total).Order("created_at DESC").Find(&list).Error }) return list, total, err } @@ -100,3 +100,14 @@ func (m *customUserModel) InsertDevice(ctx context.Context, data *Device, tx ... return conn.Create(data).Error }) } + +func (m *customUserModel) FindLatestDeviceOnlineRecord(ctx context.Context, userId int64, identifier string) (*DeviceOnlineRecord, error) { + var rec DeviceOnlineRecord + err := m.QueryNoCacheCtx(ctx, &rec, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&DeviceOnlineRecord{}).Where("user_id = ? AND identifier = ?", userId, identifier).Order("online_time DESC").First(&rec).Error + }) + if err != nil { + return nil, err + } + return &rec, nil +} diff --git a/internal/model/user/model.go b/internal/model/user/model.go index bfe8975..3536e86 100644 --- a/internal/model/user/model.go +++ b/internal/model/user/model.go @@ -103,6 +103,8 @@ type customUserLogicModel interface { DeleteDevice(ctx context.Context, id int64, tx ...*gorm.DB) error InsertDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error + FindLatestDeviceOnlineRecord(ctx context.Context, userId int64, identifier string) (*DeviceOnlineRecord, error) + ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error ClearUserCache(ctx context.Context, data ...*User) error BatchClearRelatedCache(ctx context.Context, user *User) error diff --git a/internal/svc/serviceContext.go b/internal/svc/serviceContext.go index 1a925ba..e9dfbc8 100644 --- a/internal/svc/serviceContext.go +++ b/internal/svc/serviceContext.go @@ -1,36 +1,39 @@ package svc import ( - "context" - "fmt" - "time" + "context" + "encoding/json" + "fmt" + "strconv" + "strings" + "time" - "github.com/perfect-panel/server/internal/model/client" - "github.com/perfect-panel/server/internal/model/node" - "github.com/perfect-panel/server/pkg/device" + "github.com/perfect-panel/server/internal/model/client" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/pkg/device" - "github.com/perfect-panel/server/internal/config" - "github.com/perfect-panel/server/internal/model/ads" - "github.com/perfect-panel/server/internal/model/announcement" - "github.com/perfect-panel/server/internal/model/auth" - "github.com/perfect-panel/server/internal/model/coupon" - "github.com/perfect-panel/server/internal/model/document" - "github.com/perfect-panel/server/internal/model/log" - "github.com/perfect-panel/server/internal/model/order" - "github.com/perfect-panel/server/internal/model/payment" - "github.com/perfect-panel/server/internal/model/subscribe" - "github.com/perfect-panel/server/internal/model/system" - "github.com/perfect-panel/server/internal/model/ticket" - "github.com/perfect-panel/server/internal/model/traffic" - "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/server/pkg/limit" - "github.com/perfect-panel/server/pkg/nodeMultiplier" - "github.com/perfect-panel/server/pkg/orm" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/ads" + "github.com/perfect-panel/server/internal/model/announcement" + "github.com/perfect-panel/server/internal/model/auth" + "github.com/perfect-panel/server/internal/model/coupon" + "github.com/perfect-panel/server/internal/model/document" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/order" + "github.com/perfect-panel/server/internal/model/payment" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/model/system" + "github.com/perfect-panel/server/internal/model/ticket" + "github.com/perfect-panel/server/internal/model/traffic" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/limit" + "github.com/perfect-panel/server/pkg/nodeMultiplier" + "github.com/perfect-panel/server/pkg/orm" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" - "github.com/hibiken/asynq" - "github.com/redis/go-redis/v9" - "gorm.io/gorm" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/hibiken/asynq" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" ) type ServiceContext struct { @@ -110,34 +113,68 @@ func NewServiceContext(c config.Config) *ServiceContext { TrafficLogModel: traffic.NewModel(db), AnnouncementModel: announcement.NewModel(db, rds), } - srv.DeviceManager = NewDeviceManager(srv) - return srv + srv.DeviceManager = NewDeviceManager(srv) + return srv } +func (srv *ServiceContext) SessionLimit() int64 { + cd := srv.Config.Site.CustomData + if cd != "" { + var obj map[string]interface{} + if json.Unmarshal([]byte(cd), &obj) == nil { + if v, ok := obj["deviceLimit"]; ok { + switch val := v.(type) { + case float64: + if val > 0 { + return int64(val) + } + case string: + if n, err := strconv.ParseInt(strings.TrimSpace(val), 10, 64); err == nil && n > 0 { + return n + } + } + } + if v, ok := obj["DeviceLimit"]; ok { + switch val := v.(type) { + case float64: + if val > 0 { + return int64(val) + } + case string: + if n, err := strconv.ParseInt(strings.TrimSpace(val), 10, 64); err == nil && n > 0 { + return n + } + } + } + } + } + return srv.Config.JwtAuth.MaxSessionsPerUser +} + func (srv *ServiceContext) EnforceUserSessionLimit(ctx context.Context, userId int64, newSessionId string, max int64) error { - if max <= 0 { - return nil - } - sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId) - now := time.Now().Unix() - if err := srv.Redis.ZAdd(ctx, sessionsKey, redis.Z{Score: float64(now), Member: newSessionId}).Err(); err != nil { - return err - } - count, err := srv.Redis.ZCard(ctx, sessionsKey).Result() - if err != nil { - return err - } - if count > max { - popped, err := srv.Redis.ZPopMin(ctx, sessionsKey, count-max).Result() - if err != nil { - return err - } - for _, z := range popped { - sid := fmt.Sprintf("%v", z.Member) - _ = srv.Redis.Del(ctx, fmt.Sprintf("%v:%v", config.SessionIdKey, sid)).Err() - } - } - _ = srv.Redis.Expire(ctx, sessionsKey, time.Duration(srv.Config.JwtAuth.AccessExpire)*time.Second).Err() - return nil + if max <= 0 { + return nil + } + sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId) + now := time.Now().Unix() + if err := srv.Redis.ZAdd(ctx, sessionsKey, redis.Z{Score: float64(now), Member: newSessionId}).Err(); err != nil { + return err + } + count, err := srv.Redis.ZCard(ctx, sessionsKey).Result() + if err != nil { + return err + } + if count > max { + popped, err := srv.Redis.ZPopMin(ctx, sessionsKey, count-max).Result() + if err != nil { + return err + } + for _, z := range popped { + sid := fmt.Sprintf("%v", z.Member) + _ = srv.Redis.Del(ctx, fmt.Sprintf("%v:%v", config.SessionIdKey, sid)).Err() + } + } + _ = srv.Redis.Expire(ctx, sessionsKey, time.Duration(srv.Config.JwtAuth.AccessExpire)*time.Second).Err() + return nil } diff --git a/pkg/device/device.go b/pkg/device/device.go index 9aa2239..953edd5 100644 --- a/pkg/device/device.go +++ b/pkg/device/device.go @@ -357,3 +357,18 @@ func (dm *DeviceManager) Shutdown(ctx context.Context) { return true }) } + +func (dm *DeviceManager) GetOnlineDeviceLoginTime(userID int64, deviceID string) (time.Time, bool) { + mu := dm.getUserMutex(userID) + mu.Lock() + defer mu.Unlock() + if val, ok := dm.userDevices.Load(userID); ok { + devices := val.([]*Device) + for _, d := range devices { + if d.DeviceID == deviceID { + return d.CreatedAt, true + } + } + } + return time.Time{}, false +}