Files
5.24BuYu/Assets/Framework/Tool/BeijingTimeManager.cs
T
2026-07-27 23:30:25 +08:00

251 lines
9.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Newtonsoft.Json.Linq;
using System;
using System.Collections;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Networking;
/// <summary>
/// 北京时间管理器 - 同步时记录服务器时间与本地系统时间的差值,获取时返回本地时间+差值
/// </summary>
public class BeijingTimeManager : MonoSingleton<BeijingTimeManager>
{
[Header("时间状态")]
[SerializeField] private bool _isTimeSynced = false;
[SerializeField] private TimeSpan _serverTimeOffset; // 服务器时间 - 本地系统时间
[Header("定时同步")]
[Tooltip("定时同步间隔(秒),本地系统时钟会漂移,需要定期校正")]
[SerializeField] private float _syncInterval = 30f;
[Tooltip("offset 平滑系数(0~1),越大越偏向新测量值,抗 RTT 抖动")]
[SerializeField] private float _smoothFactor = 0.3f;
private Coroutine _autoSyncCoroutine;
/// <summary>
/// 获取当前北京时间
/// </summary>
public DateTime CurrentBeijingTime
{
get
{
if (_isTimeSynced)
{
return DateTime.Now + _serverTimeOffset;
}
return DateTime.Now; // 未同步时返回本地时间
}
}
/// <summary>
/// 获取时间戳(秒)- 1970-01-01 08:00:00 到现在的秒数
/// </summary>
public long CurrentTimestamp
{
get
{
DateTime beijingTime = CurrentBeijingTime;
DateTime epoch = new DateTime(1970, 1, 1, 8, 0, 0); // 北京时间起点
return (long)(beijingTime - epoch).TotalSeconds;
}
}
/// <summary>
/// 获取时间戳(毫秒)
/// </summary>
public long CurrentTimestampMs
{
get
{
DateTime beijingTime = CurrentBeijingTime;
DateTime epoch = new DateTime(1970, 1, 1, 8, 0, 0); // 北京时间起点
return (long)(beijingTime - epoch).TotalMilliseconds;
}
}
public bool IsTimeSynced => _isTimeSynced;
public event Action<DateTime> OnTimeSynced;
private void Start()
{
SyncBeijingTime();
// 启动定时同步,修正本地系统时钟漂移
if (_autoSyncCoroutine == null)
_autoSyncCoroutine = StartCoroutine(AutoSyncLoop());
}
/// <summary>
/// 定时同步循环
/// </summary>
private IEnumerator AutoSyncLoop()
{
while (true)
{
yield return new WaitForSeconds(_syncInterval);
SyncBeijingTime();
}
}
private void OnDestroy()
{
if (_autoSyncCoroutine != null)
StopCoroutine(_autoSyncCoroutine);
}
/// <summary>
/// 同步北京时间
/// </summary>
public void SyncBeijingTime(Action action=null)
{
StartCoroutine(FetchBeijingTime(action));
}
private IEnumerator FetchBeijingTime(Action action = null)
{
string[] timeServers = new string[]
{
"http://47.109.132.12/api/Login/GetDateTime",
"https://timeapi.io/api/Time/current/zone?timeZone=Asia/Shanghai",
"http://worldtimeapi.org/api/timezone/Asia/Shanghai",
"https://quan.suning.com/getSysTime.do"
};
foreach (string url in timeServers)
{
// 记录请求开始时间,用于 RTT 补偿
DateTime requestStartTime = DateTime.Now;
using (UnityWebRequest request = UnityWebRequest.Get(url + "?_=" + DateTime.Now.Ticks))
{
request.timeout = 5;
yield return request.SendWebRequest();
// 收到响应的本地时间
DateTime requestEndTime = DateTime.Now;
if (request.result == UnityWebRequest.Result.Success)
{
string jsonText = request.downloadHandler.text;
Debug.Log($"返回原始数据:{jsonText}");
if (!jsonText.TrimStart().StartsWith("{"))
{
Debug.LogWarning("返回的不是 JSON,跳过该源");
continue;
}
try
{
JObject json = JObject.Parse(jsonText);
DateTime? serverTime = null;
if (url.Contains("47.109.132.12"))
{
// 返回格式:{"data":{"dateTime":"2026-07-26 20:02:48","milliSeconds":330,...},"code":0,"info":null}
// dateTime 只精确到秒,需要补 milliSeconds 字段
string timeStr = json["data"]?["dateTime"]?.ToString();
int ms = json["data"]?["milliSeconds"]?.Value<int>() ?? 0;
Debug.Log($"47.109.132.12 提取到的时间字符串:{timeStr} 毫秒:{ms}");
// 把服务器时间变成毫秒级时间戳打印
if (!string.IsNullOrEmpty(timeStr) &&
DateTime.TryParseExact(timeStr, "yyyy-MM-dd HH:mm:ss", null, System.Globalization.DateTimeStyles.None, out DateTime t))
{
DateTime serverTimeWithMs = t.AddMilliseconds(ms);
DateTime epoch = new DateTime(1970, 1, 1, 8, 0, 0);
long timestampMs = (long)(serverTimeWithMs - epoch).TotalMilliseconds;
Debug.Log($"47.109.132.12 服务器时间毫秒级时间戳:{timestampMs}");
}
if (!string.IsNullOrEmpty(timeStr) &&
DateTime.TryParseExact(timeStr, "yyyy-MM-dd HH:mm:ss", null, System.Globalization.DateTimeStyles.None, out DateTime t2))
{
serverTime = t2.AddMilliseconds(ms);
}
else
{
Debug.LogWarning($"无法解析时间字符串:{timeStr}");
}
}
else if (url.Contains("timeapi.io"))
{
string timeStr = json["dateTime"]?.ToString();
Debug.Log(timeStr);
if (!string.IsNullOrEmpty(timeStr))
{
serverTime = DateTime.Parse(timeStr);
}
}
else if (url.Contains("worldtimeapi"))
{
string timeStr = json["datetime"]?.ToString();
Debug.Log(timeStr);
if (!string.IsNullOrEmpty(timeStr))
{
serverTime = DateTimeOffset.Parse(timeStr)
.ToOffset(TimeSpan.FromHours(8))
.DateTime;
}
}
else if (url.Contains("suning"))
{
string timeStr = json["sysTime2"]?.ToString();
Debug.Log(timeStr);
if (!string.IsNullOrEmpty(timeStr))
{
serverTime = DateTime.ParseExact(timeStr, "yyyy-MM-dd HH:mm:ss", null);
}
}
if (serverTime.HasValue)
{
// RTT 补偿:服务器生成响应到客户端接收耗时约为 RTT/2(假设网络对称)
// 补偿后服务器时间 = 服务器响应时间 + RTT/2
TimeSpan rtt = requestEndTime - requestStartTime;
DateTime compensatedServerTime = serverTime.Value + TimeSpan.FromTicks(rtt.Ticks / 2);
TimeSpan measuredOffset = compensatedServerTime - requestEndTime;
// offset 平滑(低通滤波):抗 RTT 抖动,同时跟随系统时钟漂移
// 首次同步直接采用,后续按平滑系数加权
if (_isTimeSynced)
{
double smoothed = _serverTimeOffset.TotalMilliseconds * (1 - _smoothFactor)
+ measuredOffset.TotalMilliseconds * _smoothFactor;
_serverTimeOffset = TimeSpan.FromMilliseconds(smoothed);
}
else
{
_serverTimeOffset = measuredOffset;
_isTimeSynced = true;
}
Debug.Log($"北京时间同步成功:{CurrentBeijingTime:yyyy-MM-dd HH:mm:ss.fff},偏移量:{_serverTimeOffset.TotalSeconds:F3}s,本次测量:{measuredOffset.TotalSeconds:F3}s,RTT:{rtt.TotalSeconds:F3}s");
OnTimeSynced?.Invoke(CurrentBeijingTime);
action?.Invoke();
yield break;
}
}
catch (Exception e)
{
Debug.LogWarning($"解析失败:{e.Message}");
continue;
}
}
}
}
Debug.LogError("所有时间服务器都失败了,使用本地时间");
_serverTimeOffset = TimeSpan.Zero;
_isTimeSynced = false;
}
/// <summary>
/// 获取格式化的北京时间
/// </summary>
public string GetFormattedTime(string format = "yyyy-MM-dd HH:mm:ss")
{
return CurrentBeijingTime.ToString(format);
}
}