-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtraffic_monitor.php
More file actions
810 lines (696 loc) · 35.9 KB
/
traffic_monitor.php
File metadata and controls
810 lines (696 loc) · 35.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
<?php
/**
* 流量监控类
* 负责从API获取流量数据并保存到数据库
*/
require_once 'config.php';
require_once 'database.php';
require_once 'logger.php';
class TrafficMonitor {
private Database $db;
private Logger $logger;
private string $apiUrl;
private string $proxyHost;
private string $proxyUsername;
private string $proxyPassword;
private int $proxyPort;
public function __construct(?Database $db = null, ?Logger $logger = null) {
$this->db = $db ?? new Database();
$this->db->initializeSchema();
$this->logger = $logger ?? new Logger();
// 从配置文件获取API配置
$this->apiUrl = defined('TRAFFIC_API_URL') ? TRAFFIC_API_URL : '';
$this->proxyHost = defined('TRAFFIC_API_PROXY_HOST') ? TRAFFIC_API_PROXY_HOST : '';
$this->proxyUsername = defined('TRAFFIC_API_PROXY_USERNAME') ? TRAFFIC_API_PROXY_USERNAME : '';
$this->proxyPassword = defined('TRAFFIC_API_PROXY_PASSWORD') ? TRAFFIC_API_PROXY_PASSWORD : '';
$this->proxyPort = defined('TRAFFIC_API_PROXY_PORT') ? TRAFFIC_API_PROXY_PORT : 8080;
}
/**
* 从API获取流量数据
* @return array|false
*/
public function fetchTrafficData(): array|false {
if (empty($this->apiUrl)) {
$this->logger->error('流量监控API URL未配置');
return false;
}
$maxRetries = defined('MAX_RETRIES') ? (int)MAX_RETRIES : 3;
$retryDelayUs = defined('PROXY_RETRY_DELAY_US') ? (int)PROXY_RETRY_DELAY_US : 200000;
$lastError = '';
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
$ch = null;
try {
$ch = curl_init();
if ($ch === false) {
throw new Exception('curl初始化失败');
}
// 设置curl选项
curl_setopt($ch, CURLOPT_URL, $this->apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// 如果配置了代理认证,使用HTTP代理
if (!empty($this->proxyHost) && !empty($this->proxyUsername) && !empty($this->proxyPassword)) {
$proxyUrl = "http://{$this->proxyUsername}:{$this->proxyPassword}@{$this->proxyHost}:{$this->proxyPort}";
curl_setopt($ch, CURLOPT_PROXY, $proxyUrl);
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
if ($attempt === 1) {
$this->logger->info("使用HTTP代理: {$this->proxyHost}:{$this->proxyPort}");
}
}
// 执行请求
$response = curl_exec($ch);
if ($response === false) {
$error = curl_error($ch);
throw new Exception("API请求失败: $error");
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode !== 200) {
throw new Exception("API返回错误状态码: $httpCode");
}
curl_close($ch);
$ch = null;
// 解析JSON响应
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('JSON解析失败: ' . json_last_error_msg());
}
if ($attempt > 1) {
$this->logger->info("第{$attempt}次重试成功获取流量数据");
} else {
$this->logger->info('成功获取流量数据');
}
return $data;
} catch (Exception $e) {
if ($ch !== null) {
curl_close($ch);
$ch = null;
}
$lastError = $e->getMessage();
if ($attempt < $maxRetries) {
$this->logger->warning("获取流量数据失败 (第{$attempt}/{$maxRetries}次): {$lastError},将重试...");
usleep($retryDelayUs * $attempt); // 递增延迟
}
}
}
$this->logger->error("获取流量数据失败 (已重试{$maxRetries}次): {$lastError}");
return false;
}
/**
* 更新实时流量数据
* @return bool
*/
public function updateRealtimeTraffic(): bool {
$data = $this->fetchTrafficData();
if ($data === false) {
return false;
}
return $this->updateRealtimeTrafficWithData($data);
}
/**
* 使用已获取的 API 数据更新实时流量数据
* @param array|null $data API 返回的数据
* @return bool
*/
public function updateRealtimeTrafficWithData(?array $data): bool {
if (!$data || !is_array($data)) {
return false;
}
// API返回的数据格式:
// {
// "port": 12323, // 端口号
// "rx": 48726511916, // 接收流量(字节,累计)
// "tx": 1785605916694 // 发送流量(字节,累计)
// }
$port = isset($data['port']) ? intval($data['port']) : 0;
$rxBytes = isset($data['rx']) ? floatval($data['rx']) : 0;
$txBytes = isset($data['tx']) ? floatval($data['tx']) : 0;
// 转换为GB(1 GB = 1024^3 bytes)
$rxGB = $rxBytes / (1024 * 1024 * 1024);
$txGB = $txBytes / (1024 * 1024 * 1024);
$totalUsedGB = $txGB + $rxGB;
// 从配置获取总流量限制(如果有的话)
$totalBandwidthGB = defined('TRAFFIC_TOTAL_LIMIT_GB') ? TRAFFIC_TOTAL_LIMIT_GB : 0;
$remainingBandwidthGB = $totalBandwidthGB > 0 ? max(0, $totalBandwidthGB - $totalUsedGB) : 0;
// 计算使用百分比
$usagePercentage = $totalBandwidthGB > 0 ? ($totalUsedGB / $totalBandwidthGB) * 100 : 0;
// 保存到数据库(包含原始字节数)
$result = $this->db->saveRealtimeTraffic(
$totalBandwidthGB,
$totalUsedGB,
$remainingBandwidthGB,
$usagePercentage,
$rxBytes,
$txBytes,
$port
);
// 同时保存快照数据用于图表展示
$snapshotResult = $this->db->saveTrafficSnapshot($rxBytes, $txBytes);
if ($result) {
$this->logger->info("实时流量数据已更新: 端口={$port}, RX={$rxGB}GB, TX={$txGB}GB, 已用(RX+TX)={$totalUsedGB}GB, 使用率={$usagePercentage}%");
if ($snapshotResult) {
$this->logger->info("流量快照已保存");
}
}
return $result;
}
/**
* 更新每日流量统计
* @return bool
*/
public function updateDailyStats(): bool {
$data = $this->fetchTrafficData();
if ($data === false) {
return false;
}
return $this->updateDailyStatsWithData($data);
}
/**
* 使用已获取的 API 数据更新每日流量统计
* @param array|null $data API 返回的数据
* @return bool
*/
public function updateDailyStatsWithData(?array $data): bool {
if (!$data || !is_array($data)) {
return false;
}
$today = date('Y-m-d');
// 解析API数据
$rxBytes = isset($data['rx']) ? floatval($data['rx']) : 0;
$txBytes = isset($data['tx']) ? floatval($data['tx']) : 0;
// 转换为GB
$rxGB = $rxBytes / (1024 * 1024 * 1024);
$txGB = $txBytes / (1024 * 1024 * 1024);
$totalUsedGB = $txGB + $rxGB;
// 从配置获取总流量限制
$totalBandwidthGB = defined('TRAFFIC_TOTAL_LIMIT_GB') ? TRAFFIC_TOTAL_LIMIT_GB : 0;
$remainingBandwidthGB = $totalBandwidthGB > 0 ? max(0, $totalBandwidthGB - $totalUsedGB) : 0;
// 计算今日使用量:使用快照增量累加,避免流量重置导致的数据丢失
$dailyUsage = $this->calculateDailyUsageFromSnapshots($today);
// 如果快照数据不足,回退到使用昨天最后快照与当前API值的差值
if ($dailyUsage === false) {
$yesterday = date('Y-m-d', strtotime($today . ' -1 day'));
$yesterdayLastSnapshot = $this->db->getLastSnapshotOfDay($yesterday);
if ($yesterdayLastSnapshot) {
// 使用同源数据:API累计值 - 昨天最后快照的累计值
$yesterdayTotal = $yesterdayLastSnapshot['total_bytes'] / (1024 * 1024 * 1024);
$dailyUsage = $totalUsedGB - $yesterdayTotal;
// 检测流量重置或API数据异常:如果计算结果为负
if ($dailyUsage < 0) {
$difference = abs($dailyUsage);
// 只有差值超过阈值才认为是真正的流量重置
$resetThreshold = defined('TRAFFIC_RESET_THRESHOLD_GB') ? TRAFFIC_RESET_THRESHOLD_GB : 100;
if ($difference >= $resetThreshold) {
$dailyUsage = $totalUsedGB;
$this->logger->warning("检测到流量重置(差值{$difference}GB >= {$resetThreshold}GB),当日使用量可能不准确(丢失跨日流量)");
} else {
// 差值 < 100GB,可能是API数据异常,跳过本次更新
$this->logger->warning("API数据异常:今天累计值({$totalUsedGB}GB)比昨天快照({$yesterdayTotal}GB)少{$difference}GB,但不足{$resetThreshold}GB阈值,跳过本次更新");
return false;
}
}
} else {
// 没有昨天的快照数据,使用今天的累计值作为当日使用量
$dailyUsage = $totalUsedGB;
}
}
// 计算要存储的"已用流量"值(当月累计)
$displayUsedGB = $totalUsedGB;
$trafficReset = false;
$skipUpdate = false;
// 核心原则:今日 used_bandwidth = 昨日 used_bandwidth + 今日 daily_usage
// 每月1日或无昨日数据时,当月累计 = 当日使用量
$yesterday = date('Y-m-d', strtotime($today . ' -1 day'));
$yesterdayData = $this->db->getDailyTrafficStats($yesterday);
$isFirstDayOfMonth = (date('d') === '01');
if (!$isFirstDayOfMonth && $yesterdayData && isset($yesterdayData['used_bandwidth'])) {
$displayUsedGB = $yesterdayData['used_bandwidth'] + $dailyUsage;
$this->logger->info("当月累计:{$displayUsedGB}GB = {$yesterdayData['used_bandwidth']}GB(昨日累计) + {$dailyUsage}GB(今日使用)");
} else {
// 每月1日或无昨日数据:当月累计 = 当日使用量
$displayUsedGB = $dailyUsage;
$this->logger->info("当月累计:{$displayUsedGB}GB(无昨日当月数据,使用当日使用量)");
}
if ($dailyUsage > $totalUsedGB) {
// 流量重置(月初自动或手动重置):新计费周期从0开始
// used_bandwidth 和 daily_usage 都只算重置后的流量
$this->logger->info("检测到流量重置(新计费周期),重置前+后当日总量: {$dailyUsage}GB, 重置后API累计: {$totalUsedGB}GB");
$dailyUsage = $totalUsedGB;
$displayUsedGB = $totalUsedGB;
$remainingBandwidthGB = $totalBandwidthGB > 0 ? max(0, $totalBandwidthGB - $displayUsedGB) : 0;
$trafficReset = true;
// 只有在真正发生流量重置时才回溯更新昨天的数据
// 判断条件:今天的第一个快照值必须明显小于昨天的累计值(说明发生了重置)
$todaySnapshots = $this->db->getTrafficSnapshotsByDate($today);
if (!empty($todaySnapshots)) {
$firstSnapshot = $todaySnapshots[0];
$todayFirstValue = $firstSnapshot['total_bytes'] / (1024 * 1024 * 1024);
$yesterday = date('Y-m-d', strtotime($today . ' -1 day'));
$yesterdayData = $this->db->getDailyTrafficStats($yesterday);
// 关键判断:只有当今天首个快照值比昨天累计值少超过阈值时,才认为是真正的流量重置
$resetThreshold = defined('TRAFFIC_RESET_THRESHOLD_GB') ? TRAFFIC_RESET_THRESHOLD_GB : 100;
$difference = $yesterdayData['used_bandwidth'] - $todayFirstValue;
if ($yesterdayData && $difference >= $resetThreshold) {
// 真正的流量重置:今天的值比昨天少100GB以上,说明流量被重置了
// 这种情况下,需要将 23:55 ~ 00:00 这段跨日流量累加到昨天的数据中
$this->logger->info("检测到真正的流量重置:昨天{$yesterdayData['used_bandwidth']}GB,今天首个快照{$todayFirstValue}GB,差值{$difference}GB >= {$resetThreshold}GB");
$yesterdayLastSnapshot = $this->db->getLastSnapshotOfDay($yesterday);
if ($yesterdayLastSnapshot) {
$yesterday23_55Value = $yesterdayLastSnapshot['total_bytes'] / (1024 * 1024 * 1024);
// 计算 23:55 ~ 00:00 的跨日流量增量
// 因为发生了重置,今天00:00的值是新周期的起点
// 所以跨日流量 = 今天00:00的值(重置后的新起点)
$crossDayTraffic = $todayFirstValue;
// 昨天的最终累计值 = 昨天23:55的值 + 跨日流量
$resetBeforeValue = $yesterday23_55Value + $crossDayTraffic;
$this->logger->info("流量重置回溯计算:昨天23:55={$yesterday23_55Value}GB + 跨日流量(23:55~00:00)={$crossDayTraffic}GB = {$resetBeforeValue}GB");
// 重新计算昨天的当日使用量
// 注意:calculateDailyUsageFromSnapshots 已包含跨日增量,无需再额外加
$yesterdayDailyUsage = $this->calculateDailyUsageFromSnapshots($yesterday);
if ($yesterdayDailyUsage === false) {
// 如果无法计算,使用重置前的累计值作为当日使用量
$yesterdayDailyUsage = $resetBeforeValue;
}
// 更新昨天的已用流量和当日使用量
$yesterdayRemaining = $yesterdayData['total_bandwidth'] > 0
? max(0, $yesterdayData['total_bandwidth'] - $resetBeforeValue)
: 0;
$this->db->saveDailyTrafficStats(
$yesterday,
$yesterdayData['total_bandwidth'],
$resetBeforeValue,
$yesterdayRemaining,
$yesterdayDailyUsage
);
$this->logger->info("流量重置:回溯更新 {$yesterday} 的数据 - 已用流量: {$yesterdayData['used_bandwidth']}GB → {$resetBeforeValue}GB, 当日使用: {$yesterdayData['daily_usage']}GB → {$yesterdayDailyUsage}GB");
}
} else {
if ($yesterdayData) {
$this->logger->warning("检测到 dailyUsage > totalUsedGB,但差值({$difference}GB)小于{$resetThreshold}GB阈值,判断为API数据异常而非真正的流量重置,跳过回溯更新");
// API数据异常:今天的值比昨天小,但差值不足100GB
// 这种情况下不应该保存异常数据,跳过本次更新
if ($todayFirstValue < $yesterdayData['used_bandwidth']) {
$skipUpdate = true;
$this->logger->warning("API数据异常:今天累计值({$todayFirstValue}GB)小于昨天({$yesterdayData['used_bandwidth']}GB),跳过本次数据更新");
}
} else {
$this->logger->warning("检测到 dailyUsage > totalUsedGB,但没有昨天的数据,跳过回溯更新");
}
}
}
}
// 保存每日统计(如果检测到API数据异常则跳过)
if ($skipUpdate) {
$this->logger->info("由于API数据异常,跳过本次流量统计更新");
return false;
}
$result = $this->db->saveDailyTrafficStats(
$today,
$totalBandwidthGB,
$displayUsedGB,
$remainingBandwidthGB,
$dailyUsage
);
if ($result) {
$this->logger->info("每日流量统计已更新: 日期={$today}, 累计使用(RX+TX)={$totalUsedGB}GB, 今日使用={$dailyUsage}GB");
}
return $result;
}
/**
* 从快照数据计算当日真实流量使用量(增量累加)
* 这样即使发生流量重置,也能准确计算当日流量
* @param string $date 日期 (Y-m-d)
* @return float|false 当日使用量(GB),如果数据不足返回false
*/
private function calculateDailyUsageFromSnapshots(string $date): float|false {
// 获取当日所有快照
$snapshots = $this->db->getTrafficSnapshotsByDate($date);
if (empty($snapshots)) {
return false;
}
// 获取前一天最后一个快照(23:55)作为基准点
$yesterday = date('Y-m-d', strtotime($date . ' -1 day'));
$yesterdayLastSnapshot = $this->db->getLastSnapshotOfDay($yesterday);
$totalDailyUsage = 0;
// 统一逻辑:无论是否跨月,都从昨天最后快照开始计算
// 跨月时 API 重置会导致负增量,负增量处理逻辑会正确使用当前快照绝对值
if ($yesterdayLastSnapshot && !empty($snapshots)) {
$hasMidnightSnapshot = isset($snapshots[0]['snapshot_time']) && $snapshots[0]['snapshot_time'] === '00:00:00';
if ($hasMidnightSnapshot) {
// 有 00:00 快照时,先计算跨日增量(昨天最后快照 → 今天 00:00)
$yesterdayLastTotalGB = $yesterdayLastSnapshot['total_bytes'] / (1024 * 1024 * 1024);
$todayMidnightTotalGB = $snapshots[0]['total_bytes'] / (1024 * 1024 * 1024);
$crossDayIncrement = $todayMidnightTotalGB - $yesterdayLastTotalGB;
$crossDayMaxGB = defined('TRAFFIC_CROSSDAY_MAX_GB') ? TRAFFIC_CROSSDAY_MAX_GB : 50;
$crossDayHandling = 'zero_increment_skip';
if ($crossDayIncrement > 0) {
$totalDailyUsage += $crossDayIncrement;
$crossDayHandling = 'positive_increment_included';
if ($crossDayIncrement >= $crossDayMaxGB) {
$this->logger->warning("跨日增量异常偏大但已计入(昨天最后→今天00:00): {$crossDayIncrement}GB >= {$crossDayMaxGB}GB");
} else {
$this->logger->debug("跨日增量(昨天最后→今天00:00): {$crossDayIncrement}GB");
}
} elseif ($crossDayIncrement < 0) {
// 负增量说明发生了流量重置,将 00:00 的值作为新起点
$totalDailyUsage += $todayMidnightTotalGB;
$crossDayHandling = 'negative_increment_reset_use_midnight_absolute';
}
$enableCrossDayValidationLog = defined('TRAFFIC_CROSSDAY_VALIDATION_LOG')
? (bool) TRAFFIC_CROSSDAY_VALIDATION_LOG
: false;
if ($enableCrossDayValidationLog) {
$this->logger->info("跨日校验: date={$date}, yesterday={$yesterday}, yesterday_last={$yesterdayLastTotalGB}GB, today_00_00={$todayMidnightTotalGB}GB, cross_day_increment={$crossDayIncrement}GB, handling={$crossDayHandling}");
}
// 然后从 00:05 开始累计当天内的增量
$startIndex = 1;
} else {
// 非跨月:如果有前一天的最后快照,计算第一个点的增量
$firstSnapshot = $snapshots[0];
$increment = ($firstSnapshot['total_bytes'] - $yesterdayLastSnapshot['total_bytes']) / (1024 * 1024 * 1024);
// 如果增量为负,说明发生了流量重置,只计算第一个快照的值
if ($increment < 0) {
$totalDailyUsage += $firstSnapshot['total_bytes'] / (1024 * 1024 * 1024);
} else {
$totalDailyUsage += $increment;
}
// 从第二个快照开始计算增量
$startIndex = 1;
}
} else {
// 没有前一天的数据,只计算当天快照间的增量(不使用首个快照的绝对值,因为那是月度累计)
$this->logger->warning("无前一天快照数据,仅计算当天快照间增量(可能丢失首个快照前的流量)");
$startIndex = 1;
}
// 计算当日各快照之间的增量
for ($i = $startIndex; $i < count($snapshots); $i++) {
if ($i > 0) {
$increment = ($snapshots[$i]['total_bytes'] - $snapshots[$i-1]['total_bytes']) / (1024 * 1024 * 1024);
// 如果增量为负,说明发生了流量重置
if ($increment < 0) {
// 只累加当前快照的值(重置后的新流量)
$totalDailyUsage += $snapshots[$i]['total_bytes'] / (1024 * 1024 * 1024);
} else {
$totalDailyUsage += $increment;
}
}
}
return $totalDailyUsage;
}
/**
* 获取实时流量数据
* @return array|null
*/
public function getRealtimeTraffic(): ?array {
return $this->db->getRealtimeTraffic();
}
/**
* 获取最近N天的流量统计
* @param int $days 天数
* @return array
*/
public function getRecentStats(int $days = 30): array {
return $this->db->getRecentTrafficStats($days);
}
/**
* 获取指定日期的流量统计
* @param string $date 日期 (Y-m-d)
* @return array|null
*/
public function getStatsForDate(string $date): ?array {
return $this->db->getDailyTrafficStats($date);
}
/**
* 获取指定日期的最后一个快照
* @param string $date 日期 (Y-m-d)
* @return array|null
*/
public function getLastSnapshotOfDay(string $date): ?array {
return $this->db->getLastSnapshotOfDay($date);
}
/**
* 获取指定日期的第一个快照
* @param string $date 日期 (Y-m-d)
* @return array|null
*/
public function getFirstSnapshotOfDay(string $date): ?array {
return $this->db->getFirstSnapshotOfDay($date);
}
/**
* 获取指定日期前后N天的流量统计
* @param string $centerDate 中心日期 (Y-m-d)
* @param int $daysBefore 前面天数
* @param int $daysAfter 后面天数
* @return array
*/
public function getStatsAroundDate(string $centerDate, int $daysBefore = 7, int $daysAfter = 7): array {
$startDate = date('Y-m-d', strtotime($centerDate . " -{$daysBefore} days"));
$endDate = date('Y-m-d', strtotime($centerDate . " +{$daysAfter} days"));
return $this->db->getTrafficStatsByDateRange($startDate, $endDate);
}
/**
* 格式化流量大小(统一使用GB)
* @param float $gb 流量GB
* @return string
*/
public function formatBandwidth(float $gb): string {
return number_format($gb, 2) . ' GB';
}
/**
* 格式化百分比
* @param float $percentage 百分比
* @return string
*/
public function formatPercentage(float $percentage): string {
return number_format($percentage, 2) . '%';
}
/**
* 获取今日流量快照用于图表展示
* @return array
*/
public function getTodaySnapshots(): array {
return $this->db->getTodayTrafficSnapshots();
}
/**
* 获取指定日期的流量快照用于图表展示
* @param string $date 日期
* @return array
*/
public function getSnapshotsByDate(string $date): array {
return $this->db->getTrafficSnapshotsByDate($date);
}
/**
* 构建实时图展示上下文(统一口径)
*
* 将「前一日最后快照 -> 当日首个快照」的跨日增量折算到当日首个采样点,
* 使图表增量总和与当日使用口径一致。
*
* @param string $date 查询日期 (Y-m-d)
* @param array $snapshots 当日快照(按时间升序)
* @return array
*/
public function buildSnapshotChartContext(string $date, array $snapshots): array {
$initialIntervalGb = 0.0;
if (!empty($snapshots)) {
$yesterday = date('Y-m-d', strtotime($date . ' -1 day'));
$yesterdayLastSnapshot = $this->db->getLastSnapshotOfDay($yesterday);
if ($yesterdayLastSnapshot) {
$firstSnapshot = $snapshots[0];
$firstTotalGb = floatval($firstSnapshot['total_bytes']) / (1024 * 1024 * 1024);
$yesterdayLastTotalGb = floatval($yesterdayLastSnapshot['total_bytes']) / (1024 * 1024 * 1024);
$initialIntervalGb = $firstTotalGb - $yesterdayLastTotalGb;
// 与日统计口径一致:若出现负增量(重置),使用首个快照绝对值作为新周期起点
if ($initialIntervalGb < 0) {
$initialIntervalGb = $firstTotalGb;
}
if ($initialIntervalGb < 0) {
$initialIntervalGb = 0.0;
}
}
}
return [
'initial_interval_mb' => $initialIntervalGb * 1024,
];
}
/**
* 构建当月流量计算上下文(统一口径,供页面和 API 复用)
* @param array|null $realtimeData 实时流量数据
* @return array
*/
public function buildMonthlyTrafficContext(?array $realtimeData): array {
$totalTrafficRaw = 0.0;
if (
isset($realtimeData['rx_bytes'], $realtimeData['tx_bytes']) &&
($realtimeData['rx_bytes'] > 0 || $realtimeData['tx_bytes'] > 0)
) {
$rxBytes = floatval($realtimeData['rx_bytes']);
$txBytes = floatval($realtimeData['tx_bytes']);
$totalTrafficRaw = ($rxBytes + $txBytes) / (1024 * 1024 * 1024);
} elseif (isset($realtimeData['used_bandwidth'])) {
$totalTrafficRaw = floatval($realtimeData['used_bandwidth']);
}
$totalTraffic = $totalTrafficRaw;
$monthlyRxBytes = isset($realtimeData['rx_bytes']) ? floatval($realtimeData['rx_bytes']) : 0.0;
$monthlyTxBytes = isset($realtimeData['tx_bytes']) ? floatval($realtimeData['tx_bytes']) : 0.0;
$firstDayOfMonth = date('Y-m-01');
$lastDayOfPrevMonth = date('Y-m-d', strtotime($firstDayOfMonth . ' -1 day'));
$prevMonthLastSnapshot = $this->getLastSnapshotOfDay($lastDayOfPrevMonth);
if ($prevMonthLastSnapshot) {
$prevRxBytes = floatval($prevMonthLastSnapshot['rx_bytes']);
$prevTxBytes = floatval($prevMonthLastSnapshot['tx_bytes']);
$monthlyRx = $monthlyRxBytes - $prevRxBytes;
if ($monthlyRx >= 0) {
$monthlyRxBytes = $monthlyRx;
}
$monthlyTx = $monthlyTxBytes - $prevTxBytes;
if ($monthlyTx >= 0) {
$monthlyTxBytes = $monthlyTx;
}
$totalTraffic = ($monthlyRxBytes + $monthlyTxBytes) / (1024 * 1024 * 1024);
} else {
$prevMonthLastDayData = $this->getStatsForDate($lastDayOfPrevMonth);
if ($prevMonthLastDayData && isset($prevMonthLastDayData['used_bandwidth'])) {
$monthlyUsed = $totalTrafficRaw - floatval($prevMonthLastDayData['used_bandwidth']);
if ($monthlyUsed >= 0) {
$totalTraffic = $monthlyUsed;
}
}
}
return [
'total_traffic_raw' => $totalTrafficRaw,
'total_traffic' => $totalTraffic,
'monthly_rx_bytes' => $monthlyRxBytes,
'monthly_tx_bytes' => $monthlyTxBytes,
'prev_month_last_snapshot' => $prevMonthLastSnapshot,
];
}
/**
* 构建今日展示计算上下文(统一口径,供页面和 API 复用)
* @param float $totalTrafficRaw API 原始累计值(GB)
* @param float $totalTraffic 当月累计展示值(GB)
* @param array|null $prevMonthLastSnapshot 上月最后快照
* @return array
*/
public function buildTodayDisplayContext(float $totalTrafficRaw, float $totalTraffic, ?array $prevMonthLastSnapshot): array {
$isFirstDayOfMonth = (date('d') === '01');
$todayStr = date('Y-m-d');
$yesterdayStr = date('Y-m-d', strtotime('-1 day'));
$todayDailyUsage = 0.0;
$yesterdayUsedBandwidth = 0.0;
$yesterdayUsedBandwidthForDisplay = 0.0;
$yesterdayLastTotal = null;
$todayDailySource = 'snapshot_increment';
$yesterdayStats = $this->getStatsForDate($yesterdayStr);
if ($yesterdayStats && isset($yesterdayStats['used_bandwidth'])) {
$yesterdayUsedBandwidth = floatval($yesterdayStats['used_bandwidth']);
$yesterdayUsedBandwidthForDisplay = $yesterdayUsedBandwidth;
}
$yesterdayLastSnapshot = $this->getLastSnapshotOfDay($yesterdayStr);
if ($yesterdayLastSnapshot) {
$yesterdayLastTotal = (
floatval($yesterdayLastSnapshot['rx_bytes']) + floatval($yesterdayLastSnapshot['tx_bytes'])
) / (1024 * 1024 * 1024);
}
// 单一标准:累计链路统一使用昨日统计表中的 used_bandwidth 作为基线,
// 今日日增量使用快照算法。避免“昨日累计基线”在展示层被替换导致表格不闭合。
$snapshotDailyUsage = $this->calculateDailyUsageFromSnapshots($todayStr);
$todayUsedBandwidth = $totalTraffic;
if ($isFirstDayOfMonth) {
// 月初:优先使用快照增量;仅在快照不可用时回退 total 链路
if ($snapshotDailyUsage !== false) {
$todayDailyUsage = max(0.0, $snapshotDailyUsage);
$todayUsedBandwidth = $todayDailyUsage;
$todayDailySource = 'first_day_snapshot_daily_only';
} else {
$todayDailyUsage = max(0.0, $todayUsedBandwidth);
$todayUsedBandwidth = $todayDailyUsage;
$todayDailySource = 'first_day_total_traffic_fallback';
}
} else {
// 非月初:单一数据源策略
// 快照可用 => 今日使用 = 快照日增量;今日已用 = 昨日已用 + 今日使用
// 快照不可用 => 回退 total 链路
if ($snapshotDailyUsage !== false) {
$todayDailyUsage = max(0.0, $snapshotDailyUsage);
$todayUsedBandwidth = $yesterdayUsedBandwidthForDisplay + $todayDailyUsage;
$todayDailySource = 'snapshot_daily_only';
} else {
$todayDailyUsage = $todayUsedBandwidth - $yesterdayUsedBandwidthForDisplay;
if ($todayDailyUsage < 0) {
$todayDailyUsage = 0.0;
}
$todayUsedBandwidth = $yesterdayUsedBandwidthForDisplay + $todayDailyUsage;
$todayDailySource = 'total_traffic_fallback';
}
}
if ($todayUsedBandwidth < 0) {
$todayUsedBandwidth = $totalTraffic;
}
$todayDailyUsageForDisplay = $todayDailyUsage;
$enableDailyStandardLog = defined('TRAFFIC_DAILY_STANDARD_LOG')
? (bool) TRAFFIC_DAILY_STANDARD_LOG
: false;
if ($enableDailyStandardLog) {
$this->logger->info(
"单一口径校验: date={$todayStr}, source={$todayDailySource}, yesterday_used={$yesterdayUsedBandwidthForDisplay}GB, today_daily={$todayDailyUsage}GB, today_used={$todayUsedBandwidth}GB, snapshot_daily=" . (($snapshotDailyUsage !== false) ? $snapshotDailyUsage : 'false') . "GB"
);
}
return [
'is_first_day_of_month' => $isFirstDayOfMonth,
'yesterday_date' => $yesterdayStr,
'today_daily_usage' => $todayDailyUsage,
'today_used_bandwidth' => $todayUsedBandwidth,
'yesterday_used_bandwidth' => $yesterdayUsedBandwidth,
'yesterday_used_bandwidth_for_display' => $yesterdayUsedBandwidthForDisplay,
'today_daily_usage_for_display' => $todayDailyUsageForDisplay,
];
}
/**
* 构建 Proxy-Status 页面/API 的统一展示上下文
*
* 目标:从单一入口计算并返回「顶部总量 + 今日行 + RX/TX 详情」所需数据,
* 避免页面层各自修补导致口径漂移。
*
* @param array|null $realtimeData 实时流量数据
* @return array
*/
public function buildProxyStatusDisplayContext(?array $realtimeData): array {
$monthlyContext = $this->buildMonthlyTrafficContext($realtimeData);
$todayContext = $this->buildTodayDisplayContext(
$monthlyContext['total_traffic_raw'],
$monthlyContext['total_traffic'],
$monthlyContext['prev_month_last_snapshot']
);
// 统一展示总量:确保与今日行保持一致,不出现“顶部 < 今日表格”的回退
$displayMonthlyUsed = max(
floatval($monthlyContext['total_traffic']),
floatval($todayContext['today_used_bandwidth'])
);
// 统一展示 RX/TX:确保 RX + TX = displayMonthlyUsed
$monthlyRxBytes = floatval($monthlyContext['monthly_rx_bytes']);
$monthlyTxBytes = floatval($monthlyContext['monthly_tx_bytes']);
$displayMonthlyRxBytes = $monthlyRxBytes;
$displayMonthlyTxBytes = $monthlyTxBytes;
$rawMonthlyUsed = ($monthlyRxBytes + $monthlyTxBytes) / (1024 * 1024 * 1024);
if ($displayMonthlyUsed > $rawMonthlyUsed) {
if ($rawMonthlyUsed > 0) {
$scale = $displayMonthlyUsed / $rawMonthlyUsed;
$displayMonthlyRxBytes = $monthlyRxBytes * $scale;
$displayMonthlyTxBytes = $monthlyTxBytes * $scale;
} else {
// 无可用 RX/TX 基线时,退化为总量展示,保证 UI 数学关系成立
$displayMonthlyRxBytes = 0.0;
$displayMonthlyTxBytes = $displayMonthlyUsed * 1024 * 1024 * 1024;
}
}
return [
'monthly_context' => $monthlyContext,
'today_context' => $todayContext,
'display_monthly_used' => $displayMonthlyUsed,
'display_monthly_rx_bytes' => $displayMonthlyRxBytes,
'display_monthly_tx_bytes' => $displayMonthlyTxBytes,
];
}
}