Add: Add a WebSocket connection to monitor the app's online status.

This commit is contained in:
EUForest 2025-11-06 15:35:49 +08:00
parent 8cce9b95b4
commit d1a8662095
2 changed files with 199 additions and 0 deletions

View File

@ -0,0 +1,115 @@
package user
import (
"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"
)
type DeviceOnlineStatisticsLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Device Online Statistics
func NewDeviceOnlineStatisticsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeviceOnlineStatisticsLogic {
return &DeviceOnlineStatisticsLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DeviceOnlineStatisticsLogic) DeviceOnlineStatistics() (resp *types.GetDeviceOnlineStatsResponse, err error) {
u := l.ctx.Value(constant.CtxKeyUser).(*user.User)
//获取历史最长在线时间
var OnlineSeconds int64
if err := l.svcCtx.DB.Model(user.DeviceOnlineRecord{}).Where("user_id = ?", u.Id).Select("online_seconds").Order("online_seconds desc").Limit(1).Scan(&OnlineSeconds).Error; err != nil {
l.Logger.Error(err)
}
//获取历史连续最长在线天数
var DurationDays int64
if err := l.svcCtx.DB.Model(user.DeviceOnlineRecord{}).Where("user_id = ?", u.Id).Select("duration_days").Order("duration_days desc").Limit(1).Scan(&DurationDays).Error; err != nil {
l.Logger.Error(err)
}
//获取近七天在线情况
var userOnlineRecord []user.DeviceOnlineRecord
if err := l.svcCtx.DB.Model(&userOnlineRecord).Where("user_id = ? and created_at >= ?", u.Id, time.Now().AddDate(0, 0, -7).Format(time.DateTime)).Order("created_at desc").Find(&userOnlineRecord).Error; err != nil {
l.Logger.Error(err)
}
//获取当前连续在线天数
var currentContinuousDays int64
if len(userOnlineRecord) > 0 {
currentContinuousDays = userOnlineRecord[0].DurationDays
} else {
currentContinuousDays = 1
}
var dates []string
for i := 0; i < 7; i++ {
date := time.Now().AddDate(0, 0, -i).Format(time.DateOnly)
dates = append(dates, date)
}
onlineDays := make(map[string]types.WeeklyStat)
for _, record := range userOnlineRecord {
//获取近七天在线情况
onlineTime := record.OnlineTime.Format(time.DateOnly)
if weeklyStat, ok := onlineDays[onlineTime]; ok {
weeklyStat.Hours += float64(record.OnlineSeconds)
onlineDays[onlineTime] = weeklyStat
} else {
onlineDays[onlineTime] = types.WeeklyStat{
Hours: float64(record.OnlineSeconds),
//根据日期获取周几
DayName: record.OnlineTime.Weekday().String(),
}
}
}
//补全不存在的日期
for _, date := range dates {
if _, ok := onlineDays[date]; !ok {
onlineTime, _ := time.Parse(time.DateOnly, date)
onlineDays[date] = types.WeeklyStat{
DayName: onlineTime.Weekday().String(),
}
}
}
var keys []string
for key := range onlineDays {
keys = append(keys, key)
}
//排序
sort.Strings(keys)
var weeklyStats []types.WeeklyStat
for index, key := range keys {
weeklyStat := onlineDays[key]
weeklyStat.Day = index + 1
weeklyStat.Hours = weeklyStat.Hours / float64(3600)
weeklyStats = append(weeklyStats, weeklyStat)
}
resp = &types.GetDeviceOnlineStatsResponse{
WeeklyStats: weeklyStats,
ConnectionRecords: types.ConnectionRecords{
CurrentContinuousDays: currentContinuousDays,
HistoryContinuousDays: DurationDays,
LongestSingleConnection: OnlineSeconds / 60,
},
}
return
}

View File

@ -0,0 +1,84 @@
package ws
import (
"context"
sysErr "errors"
"time"
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/logger"
)
type DeviceWsConnectLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Webosocket Device Connect
func NewDeviceWsConnectLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeviceWsConnectLogic {
return &DeviceWsConnectLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DeviceWsConnectLogic) DeviceWsConnect(c *gin.Context) error {
value := l.ctx.Value(constant.CtxKeyIdentifier)
if value == nil || value.(string) == "" {
l.Errorf("DeviceWsConnectLogic DeviceWsConnect identifier is empty")
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "identifier is empty")
}
identifier := value.(string)
_, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, identifier)
if err != nil && !sysErr.Is(err, gorm.ErrRecordNotFound) {
l.Errorf("DeviceWsConnectLogic DeviceWsConnect FindOneDeviceByIdentifier err: %v", err)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error())
}
value = l.ctx.Value(constant.CtxKeyUser)
if value == nil {
l.Errorf("DeviceWsConnectLogic DeviceWsConnect value is nil")
return nil
}
userInfo := value.(*user.User)
if sysErr.Is(err, gorm.ErrRecordNotFound) {
device := user.Device{
Identifier: identifier,
UserId: userInfo.Id,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Online: true,
Enabled: true,
}
err := l.svcCtx.UserModel.InsertDevice(l.ctx, &device)
if err != nil {
l.Errorf("DeviceWsConnectLogic DeviceWsConnect InsertDevice err: %v", err)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), err.Error())
}
}
//默认在线设备1
maxDevice := 3
subscribe, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, userInfo.Id, 1, 2)
if err == nil {
for _, sub := range subscribe {
if time.Now().Before(sub.ExpireTime) {
deviceLimit := int(sub.Subscribe.DeviceLimit)
if deviceLimit > maxDevice {
maxDevice = deviceLimit
}
}
}
}
l.svcCtx.DeviceManager.AddDevice(c.Writer, c.Request, l.ctx.Value(constant.CtxKeySessionID).(string), userInfo.Id, identifier, maxDevice)
return nil
}