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

添加FindLatestDeviceOnlineRecord接口用于查询设备最新在线记录
实现GetOnlineDeviceLoginTime方法获取设备登录时间
优化设备列表查询按最后活动时间排序
移除未使用的依赖项
This commit is contained in:
shanshanzhong 2025-11-27 23:24:48 -08:00
parent 236fa6c4e6
commit 2442831cd7
12 changed files with 210 additions and 130 deletions

3
go.mod
View File

@ -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
)

10
go.sum
View File

@ -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=

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) {

View File

@ -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 {

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
}

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
}

View File

@ -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("邮箱已存在,将设备转移到现有邮箱用户",

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}