2026-03-18 17:26:14 +08:00
|
|
|
|
using Newtonsoft.Json.Linq;
|
2026-07-27 23:30:25 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using Unity.VisualScripting;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.Networking;
|
2026-03-09 09:25:01 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-07-25 16:13:21 +08:00
|
|
|
|
/// 北京时间管理器 - 同步时记录服务器时间与本地系统时间的差值,获取时返回本地时间+差值
|
2026-03-09 09:25:01 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class BeijingTimeManager : MonoSingleton<BeijingTimeManager>
|
|
|
|
|
|
{
|
|
|
|
|
|
[Header("时间状态")]
|
2026-03-18 17:26:14 +08:00
|
|
|
|
[SerializeField] private bool _isTimeSynced = false;
|
2026-07-25 16:13:21 +08:00
|
|
|
|
[SerializeField] private TimeSpan _serverTimeOffset; // 服务器时间 - 本地系统时间
|
2026-03-09 09:25:01 +08:00
|
|
|
|
|
2026-07-27 23:30:25 +08:00
|
|
|
|
[Header("定时同步")]
|
|
|
|
|
|
[Tooltip("定时同步间隔(秒),本地系统时钟会漂移,需要定期校正")]
|
|
|
|
|
|
[SerializeField] private float _syncInterval = 30f;
|
|
|
|
|
|
[Tooltip("offset 平滑系数(0~1),越大越偏向新测量值,抗 RTT 抖动")]
|
|
|
|
|
|
[SerializeField] private float _smoothFactor = 0.3f;
|
|
|
|
|
|
|
|
|
|
|
|
private Coroutine _autoSyncCoroutine;
|
|
|
|
|
|
|
2026-03-18 17:26:14 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取当前北京时间
|
|
|
|
|
|
/// </summary>
|
2026-03-09 09:25:01 +08:00
|
|
|
|
public DateTime CurrentBeijingTime
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_isTimeSynced)
|
|
|
|
|
|
{
|
2026-07-25 16:13:21 +08:00
|
|
|
|
return DateTime.Now + _serverTimeOffset;
|
2026-03-09 09:25:01 +08:00
|
|
|
|
}
|
2026-03-18 17:26:14 +08:00
|
|
|
|
return DateTime.Now; // 未同步时返回本地时间
|
2026-03-09 09:25:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-03-18 17:26:14 +08:00
|
|
|
|
/// 获取时间戳(秒)- 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();
|
2026-07-27 23:30:25 +08:00
|
|
|
|
// 启动定时同步,修正本地系统时钟漂移
|
|
|
|
|
|
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);
|
2026-03-18 17:26:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 同步北京时间
|
2026-03-09 09:25:01 +08:00
|
|
|
|
/// </summary>
|
2026-07-27 23:30:25 +08:00
|
|
|
|
public void SyncBeijingTime(Action action=null)
|
2026-03-09 09:25:01 +08:00
|
|
|
|
{
|
2026-07-27 23:30:25 +08:00
|
|
|
|
StartCoroutine(FetchBeijingTime(action));
|
2026-03-09 09:25:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-27 23:30:25 +08:00
|
|
|
|
private IEnumerator FetchBeijingTime(Action action = null)
|
2026-03-09 09:25:01 +08:00
|
|
|
|
{
|
|
|
|
|
|
string[] timeServers = new string[]
|
|
|
|
|
|
{
|
2026-07-26 22:00:38 +08:00
|
|
|
|
"http://47.109.132.12/api/Login/GetDateTime",
|
2026-05-01 21:59:16 +08:00
|
|
|
|
"https://timeapi.io/api/Time/current/zone?timeZone=Asia/Shanghai",
|
|
|
|
|
|
"http://worldtimeapi.org/api/timezone/Asia/Shanghai",
|
|
|
|
|
|
"https://quan.suning.com/getSysTime.do"
|
2026-03-09 09:25:01 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
foreach (string url in timeServers)
|
|
|
|
|
|
{
|
2026-07-27 16:59:22 +08:00
|
|
|
|
// 记录请求开始时间,用于 RTT 补偿
|
|
|
|
|
|
DateTime requestStartTime = DateTime.Now;
|
2026-05-01 21:59:16 +08:00
|
|
|
|
using (UnityWebRequest request = UnityWebRequest.Get(url + "?_=" + DateTime.Now.Ticks))
|
2026-03-09 09:25:01 +08:00
|
|
|
|
{
|
2026-03-18 17:26:14 +08:00
|
|
|
|
request.timeout = 5;
|
2026-03-09 09:25:01 +08:00
|
|
|
|
yield return request.SendWebRequest();
|
2026-07-27 16:59:22 +08:00
|
|
|
|
// 收到响应的本地时间
|
|
|
|
|
|
DateTime requestEndTime = DateTime.Now;
|
2026-03-09 09:25:01 +08:00
|
|
|
|
|
2026-03-18 17:26:14 +08:00
|
|
|
|
if (request.result == UnityWebRequest.Result.Success)
|
2026-03-09 09:25:01 +08:00
|
|
|
|
{
|
2026-05-01 21:59:16 +08:00
|
|
|
|
string jsonText = request.downloadHandler.text;
|
|
|
|
|
|
Debug.Log($"返回原始数据:{jsonText}");
|
|
|
|
|
|
|
|
|
|
|
|
if (!jsonText.TrimStart().StartsWith("{"))
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning("返回的不是 JSON,跳过该源");
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 17:26:14 +08:00
|
|
|
|
try
|
2026-03-09 09:25:01 +08:00
|
|
|
|
{
|
2026-05-01 21:59:16 +08:00
|
|
|
|
JObject json = JObject.Parse(jsonText);
|
2026-07-27 16:59:22 +08:00
|
|
|
|
DateTime? serverTime = null;
|
2026-05-01 21:59:16 +08:00
|
|
|
|
|
2026-07-26 22:00:38 +08:00
|
|
|
|
if (url.Contains("47.109.132.12"))
|
|
|
|
|
|
{
|
2026-07-27 16:59:22 +08:00
|
|
|
|
// 返回格式:{"data":{"dateTime":"2026-07-26 20:02:48","milliSeconds":330,...},"code":0,"info":null}
|
|
|
|
|
|
// dateTime 只精确到秒,需要补 milliSeconds 字段
|
2026-07-26 22:00:38 +08:00
|
|
|
|
string timeStr = json["data"]?["dateTime"]?.ToString();
|
2026-07-27 16:59:22 +08:00
|
|
|
|
int ms = json["data"]?["milliSeconds"]?.Value<int>() ?? 0;
|
|
|
|
|
|
Debug.Log($"47.109.132.12 提取到的时间字符串:{timeStr} 毫秒:{ms}");
|
2026-07-26 22:00:38 +08:00
|
|
|
|
|
2026-07-27 23:30:25 +08:00
|
|
|
|
// 把服务器时间变成毫秒级时间戳打印
|
2026-07-26 22:00:38 +08:00
|
|
|
|
if (!string.IsNullOrEmpty(timeStr) &&
|
2026-07-27 16:59:22 +08:00
|
|
|
|
DateTime.TryParseExact(timeStr, "yyyy-MM-dd HH:mm:ss", null, System.Globalization.DateTimeStyles.None, out DateTime t))
|
2026-07-26 22:00:38 +08:00
|
|
|
|
{
|
2026-07-27 23:30:25 +08:00
|
|
|
|
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);
|
2026-07-26 22:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning($"无法解析时间字符串:{timeStr}");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (url.Contains("timeapi.io"))
|
2026-05-01 21:59:16 +08:00
|
|
|
|
{
|
|
|
|
|
|
string timeStr = json["dateTime"]?.ToString();
|
|
|
|
|
|
Debug.Log(timeStr);
|
|
|
|
|
|
if (!string.IsNullOrEmpty(timeStr))
|
|
|
|
|
|
{
|
2026-07-27 16:59:22 +08:00
|
|
|
|
serverTime = DateTime.Parse(timeStr);
|
2026-05-01 21:59:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (url.Contains("worldtimeapi"))
|
|
|
|
|
|
{
|
|
|
|
|
|
string timeStr = json["datetime"]?.ToString();
|
|
|
|
|
|
Debug.Log(timeStr);
|
|
|
|
|
|
if (!string.IsNullOrEmpty(timeStr))
|
|
|
|
|
|
{
|
2026-07-27 16:59:22 +08:00
|
|
|
|
serverTime = DateTimeOffset.Parse(timeStr)
|
2026-05-01 21:59:16 +08:00
|
|
|
|
.ToOffset(TimeSpan.FromHours(8))
|
|
|
|
|
|
.DateTime;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (url.Contains("suning"))
|
2026-03-09 09:25:01 +08:00
|
|
|
|
{
|
2026-03-18 17:26:14 +08:00
|
|
|
|
string timeStr = json["sysTime2"]?.ToString();
|
2026-05-01 21:59:16 +08:00
|
|
|
|
Debug.Log(timeStr);
|
2026-03-18 17:26:14 +08:00
|
|
|
|
if (!string.IsNullOrEmpty(timeStr))
|
|
|
|
|
|
{
|
2026-07-27 16:59:22 +08:00
|
|
|
|
serverTime = DateTime.ParseExact(timeStr, "yyyy-MM-dd HH:mm:ss", null);
|
2026-03-18 17:26:14 +08:00
|
|
|
|
}
|
2026-03-09 09:25:01 +08:00
|
|
|
|
}
|
2026-07-27 16:59:22 +08:00
|
|
|
|
|
|
|
|
|
|
if (serverTime.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
// RTT 补偿:服务器生成响应到客户端接收耗时约为 RTT/2(假设网络对称)
|
|
|
|
|
|
// 补偿后服务器时间 = 服务器响应时间 + RTT/2
|
|
|
|
|
|
TimeSpan rtt = requestEndTime - requestStartTime;
|
|
|
|
|
|
DateTime compensatedServerTime = serverTime.Value + TimeSpan.FromTicks(rtt.Ticks / 2);
|
2026-07-27 23:30:25 +08:00
|
|
|
|
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");
|
2026-07-27 16:59:22 +08:00
|
|
|
|
OnTimeSynced?.Invoke(CurrentBeijingTime);
|
2026-07-27 23:30:25 +08:00
|
|
|
|
action?.Invoke();
|
|
|
|
|
|
|
2026-07-27 16:59:22 +08:00
|
|
|
|
yield break;
|
|
|
|
|
|
}
|
2026-03-09 09:25:01 +08:00
|
|
|
|
}
|
2026-03-18 17:26:14 +08:00
|
|
|
|
catch (Exception e)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogWarning($"解析失败:{e.Message}");
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-03-09 09:25:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 17:26:14 +08:00
|
|
|
|
Debug.LogError("所有时间服务器都失败了,使用本地时间");
|
2026-07-25 16:13:21 +08:00
|
|
|
|
_serverTimeOffset = TimeSpan.Zero;
|
2026-03-18 17:26:14 +08:00
|
|
|
|
_isTimeSynced = false;
|
2026-03-09 09:25:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 21:59:16 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-09 09:25:01 +08:00
|
|
|
|
/// <summary>
|
2026-03-18 17:26:14 +08:00
|
|
|
|
/// 获取格式化的北京时间
|
2026-03-09 09:25:01 +08:00
|
|
|
|
/// </summary>
|
2026-03-18 17:26:14 +08:00
|
|
|
|
public string GetFormattedTime(string format = "yyyy-MM-dd HH:mm:ss")
|
2026-03-09 09:25:01 +08:00
|
|
|
|
{
|
2026-03-18 17:26:14 +08:00
|
|
|
|
return CurrentBeijingTime.ToString(format);
|
2026-03-09 09:25:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|