fix(api): 修复节点流量限额共享统计与父子显隐联动

统一节点流量统计与限额展示口径,节点详情新增昨日流量,
并让今日、昨日和本月使用清晰的半开时间窗口聚合

同 machine_id 或同 host 的节点现在共享当前账期已用流量,
管理端优先使用后端 traffic_limit_snapshot 展示月额度状态,
mi-node 下发的 current_used 也改为共享账期统计

新增 parent_auto_hidden 标记与父节点显隐联动服务,父节点
因自动上线或流量限额变为不可展示时会隐藏当前显示的子节点,
恢复时只恢复这批自动隐藏的子节点,避免覆盖手动操作
This commit is contained in:
yinjianm
2026-04-29 02:24:57 +08:00
parent 922e86070d
commit e847252e12
27 changed files with 2078 additions and 47 deletions
+18
View File
@@ -874,10 +874,26 @@ export interface AdminNodeMetrics {
export interface AdminNodeTrafficStats {
today: TrafficAmount
yesterday: TrafficAmount
month: TrafficAmount
total: TrafficAmount
}
export interface AdminNodeTrafficLimitSnapshot {
enabled: boolean
limit: number
used: number
percent: number
suspended: boolean
last_reset_at?: number
cycle_start_at?: number
next_reset_at?: number
suspended_at?: number
status?: string
scope_key?: string
scope_node_ids?: number[]
}
export interface AdminNodeRateTimeRange {
start: string
end: string
@@ -909,6 +925,7 @@ export interface AdminNodeItem {
traffic_limit_next_reset_at?: number | null
traffic_limit_suspended_at?: number | null
enabled?: boolean
machine_id?: number | null
parent_id?: number | null
rate?: number | null
transfer_enable?: number | null
@@ -926,6 +943,7 @@ export interface AdminNodeItem {
last_push_at?: number | null
metrics?: AdminNodeMetrics | null
traffic_stats?: AdminNodeTrafficStats | null
traffic_limit_snapshot?: AdminNodeTrafficLimitSnapshot | null
groups?: AdminServerGroupItem[]
parent?: AdminNodeParentRef | null
gfw_check?: AdminNodeGfwCheck | null
+26 -7
View File
@@ -22,7 +22,7 @@ export interface NodeGfwMeta {
}
export interface NodeTrafficDetail {
key: 'today' | 'month' | 'total'
key: 'today' | 'yesterday' | 'month' | 'total'
label: string
upload: string
download: string
@@ -70,6 +70,15 @@ function normalizeTrafficValue(value: unknown): number {
return Number.isFinite(normalized) && normalized > 0 ? normalized : 0
}
function normalizeOptionalTrafficValue(value: unknown): number | null {
if (value === null || value === undefined || value === '') {
return null
}
const normalized = Number(value)
return Number.isFinite(normalized) && normalized >= 0 ? normalized : null
}
function normalizeTrafficAmount(amount?: TrafficAmountLike | null): TrafficAmount {
const upload = normalizeTrafficValue(amount?.upload)
const download = normalizeTrafficValue(amount?.download)
@@ -255,6 +264,7 @@ export function getNodeTrafficDetails(node: AdminNodeItem): NodeTrafficDetail[]
const rows: Array<{ key: NodeTrafficDetail['key']; label: string; source?: TrafficAmountLike | null }> = [
{ key: 'today', label: '今日', source: stats?.today },
{ key: 'yesterday', label: '昨日', source: stats?.yesterday },
{ key: 'month', label: '本月', source: stats?.month },
{ key: 'total', label: '累计', source: stats?.total ?? totalFallback },
]
@@ -272,16 +282,25 @@ export function getNodeTrafficDetails(node: AdminNodeItem): NodeTrafficDetail[]
}
export function getNodeTrafficLimitDetail(node: AdminNodeItem): NodeTrafficLimitDetail {
const limit = normalizeTrafficValue(node.transfer_enable)
const snapshot = node.traffic_limit_snapshot
const metrics = node.metrics?.traffic_limit
const used = normalizeTrafficValue(metrics?.used) || normalizeTrafficValue(node.u) + normalizeTrafficValue(node.d)
const suspended = Boolean(metrics?.suspended) || node.traffic_limit_status === 'suspended'
const nextResetAt = normalizeTrafficValue(metrics?.next_reset_at) || normalizeTrafficValue(node.traffic_limit_next_reset_at)
const limit = normalizeOptionalTrafficValue(snapshot?.limit)
?? normalizeOptionalTrafficValue(metrics?.limit)
?? normalizeTrafficValue(node.transfer_enable)
const used = normalizeOptionalTrafficValue(snapshot?.used)
?? normalizeOptionalTrafficValue(metrics?.used)
?? normalizeTrafficValue(node.u) + normalizeTrafficValue(node.d)
const status = normalizeText(snapshot?.status || metrics?.status || node.traffic_limit_status)
const suspended = Boolean(snapshot?.suspended) || Boolean(metrics?.suspended) || status === 'suspended'
const nextResetAt = normalizeOptionalTrafficValue(snapshot?.next_reset_at)
?? normalizeOptionalTrafficValue(metrics?.next_reset_at)
?? normalizeTrafficValue(node.traffic_limit_next_reset_at)
const percent = limit > 0 ? Math.min(100, Math.round((used / limit) * 100)) : 0
const enabled = (snapshot ? Boolean(snapshot.enabled) : Boolean(node.traffic_limit_enabled)) && limit > 0
let statusLabel = '未启用'
let tagType: NodeTrafficLimitDetail['tagType'] = 'info'
if (Boolean(node.traffic_limit_enabled) && limit > 0) {
if (enabled) {
if (suspended) {
statusLabel = '已限额'
tagType = 'danger'
@@ -295,7 +314,7 @@ export function getNodeTrafficLimitDetail(node: AdminNodeItem): NodeTrafficLimit
}
return {
enabled: Boolean(node.traffic_limit_enabled) && limit > 0,
enabled,
used: formatTrafficBytes(used),
limit: formatTrafficBytes(limit),
percent,