mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-11-04 00:40:20 +00:00
Add persisted online sessions, configurable retention and online history statistics with lifecycle, recovery, timezone and concurrency regression coverage. Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
122 lines
3.9 KiB
Go
122 lines
3.9 KiB
Go
package controller
|
||
|
||
import (
|
||
"strconv"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
|
||
"github.com/nezhahq/nezha/model"
|
||
"github.com/nezhahq/nezha/service/singleton"
|
||
)
|
||
|
||
// Get server online history
|
||
// @Summary Get server online history
|
||
// @Security BearerAuth
|
||
// @Schemes
|
||
// @Description Get online periods and aggregated uptime statistics of a specific server
|
||
// @Tags common
|
||
// @Param id path uint true "Server ID"
|
||
// @Param days query int false "Query window in days (default: configured online_stats_days, clamped to it)"
|
||
// @Produce json
|
||
// @Success 200 {object} model.CommonResponse[model.OnlineHistoryResponse]
|
||
// @Router /server/{id}/online-history [get]
|
||
func getServerOnlineHistory(c *gin.Context) (*model.OnlineHistoryResponse, error) {
|
||
idStr := c.Param("id")
|
||
serverID, err := strconv.ParseUint(idStr, 10, 64)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
server, ok := singleton.ServerShared.Get(serverID)
|
||
if !ok || server == nil {
|
||
return nil, singleton.Localizer.ErrorT("server not found")
|
||
}
|
||
if !userCanViewServer(c, server) {
|
||
return nil, singleton.Localizer.ErrorT("unauthorized")
|
||
}
|
||
_, isMember := c.Get(model.CtxKeyAuthorizedUser)
|
||
|
||
maxDays := singleton.Conf.OnlineHistoryRetentionDays()
|
||
if maxDays < model.MinOnlineStatsDays || maxDays > model.MaxOnlineStatsDays {
|
||
maxDays = model.DefaultOnlineStatsDays
|
||
}
|
||
days := maxDays
|
||
if daysStr := c.Query("days"); daysStr != "" {
|
||
d, err := strconv.Atoi(daysStr)
|
||
if err != nil || d < 1 {
|
||
return nil, singleton.Localizer.ErrorT("invalid days parameter")
|
||
}
|
||
// 查询窗口收敛到后台设置的统计范围内
|
||
if d > maxDays {
|
||
d = maxDays
|
||
}
|
||
days = d
|
||
}
|
||
|
||
if !isMember && days > 1 {
|
||
return nil, singleton.Localizer.ErrorT("unauthorized: only 1d data available for guests")
|
||
}
|
||
|
||
now := time.Now()
|
||
windowEnd := now
|
||
windowStart := model.OnlineHistoryWindowStart(now, days, singleton.Loc)
|
||
|
||
sessions, err := singleton.OnlineHistoryShared.Sessions(serverID, windowStart, windowEnd)
|
||
if err != nil {
|
||
return nil, newGormError("%v", err)
|
||
}
|
||
|
||
periods := model.ClipOnlineSessions(sessions, windowStart, windowEnd)
|
||
daily := model.DailyOnlineBreakdown(periods, windowStart, windowEnd, singleton.Loc)
|
||
// 总时长取各日之和,与 daily[] 严格对账(每日累计原始时长后统一取整,
|
||
// 口径见 DailyOnlineBreakdown)
|
||
var totalSeconds uint64
|
||
for _, d := range daily {
|
||
totalSeconds += d.OnlineSeconds
|
||
}
|
||
windowSeconds := uint64(windowEnd.Sub(windowStart).Seconds())
|
||
onlineRate := 0.0
|
||
if windowSeconds > 0 {
|
||
onlineRate = float64(totalSeconds) / float64(windowSeconds) * 100
|
||
}
|
||
|
||
runtime := server.RuntimeSnapshot()
|
||
currentlyOnline := !runtime.LastActive.IsZero() && now.Sub(runtime.LastActive) <= model.OnlineActiveWindow
|
||
var lastActive *time.Time
|
||
if !runtime.LastActive.IsZero() {
|
||
lastActiveUTC := runtime.LastActive.UTC()
|
||
lastActive = &lastActiveUTC
|
||
}
|
||
|
||
// 响应统一以 UTC 序列化,避免窗口时间与库中读回的会话时间混用
|
||
// 本地偏移与 Z 两种写法(时段为值拷贝,不影响调用方)
|
||
utcPeriods := make([]model.OnlinePeriod, len(periods))
|
||
for i, p := range periods {
|
||
utcPeriods[i] = model.OnlinePeriod{Start: p.Start.UTC(), End: p.End.UTC(), Ongoing: p.Ongoing}
|
||
}
|
||
|
||
return &model.OnlineHistoryResponse{
|
||
ServerID: serverID,
|
||
ServerName: server.Name,
|
||
Days: days,
|
||
MaxDays: maxDays,
|
||
Timezone: singleton.Loc.String(),
|
||
TzOffset: tzOffsetSeconds(now, singleton.Loc),
|
||
WindowStart: windowStart.UTC(),
|
||
WindowEnd: windowEnd.UTC(),
|
||
TotalOnlineSeconds: totalSeconds,
|
||
OnlineRate: onlineRate,
|
||
CurrentlyOnline: currentlyOnline,
|
||
LastActive: lastActive,
|
||
Periods: utcPeriods,
|
||
Daily: daily,
|
||
}, nil
|
||
}
|
||
|
||
// tzOffsetSeconds 返回指定时区在给定时刻相对 UTC 的偏移秒数。
|
||
func tzOffsetSeconds(at time.Time, loc *time.Location) int {
|
||
_, offset := at.In(loc).Zone()
|
||
return offset
|
||
}
|