Files
go-taobao/utils/timesync.go
2026-02-09 16:05:56 +08:00

217 lines
5.6 KiB
Go
Raw Permalink 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.
package utils
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sort"
"strconv"
"strings"
"time"
"go-taobao/config"
)
// TimeSync 时间同步器,计算本地时钟与服务器的时间偏移
type TimeSync struct {
// offset = 服务器时间 - 本地时间
Offset time.Duration
// 使用的时间源名称
Source string
}
// taobaoH5Resp 淘宝 H5 时间 API 响应
type taobaoH5Resp struct {
Data struct {
T string `json:"t"`
} `json:"data"`
}
// suningTimeResp 苏宁时间 API 响应
type suningTimeResp struct {
SysTime2 string `json:"sysTime2"` // 格式: 2026-02-09 14:30:00
}
// NewTimeSync 创建时间同步器,依次尝试多个时间源
func NewTimeSync(samples int) (*TimeSync, error) {
if samples < 1 {
samples = 5
}
// 依次尝试每个时间源
for _, api := range config.TimeAPIs {
log.Printf("[时间同步] 尝试时间源: %s (%s)", api.Name, api.URL)
offsets := make([]time.Duration, 0, samples)
for i := 0; i < samples; i++ {
offset, err := fetchOffset(api)
if err != nil {
log.Printf("[时间同步] %s 第 %d 次采样失败: %v", api.Name, i+1, err)
continue
}
offsets = append(offsets, offset)
log.Printf("[时间同步] %s 第 %d 次采样: 偏移 = %v", api.Name, i+1, offset)
if i < samples-1 {
time.Sleep(200 * time.Millisecond)
}
}
if len(offsets) == 0 {
log.Printf("[时间同步] %s 所有采样失败,尝试下一个时间源", api.Name)
continue
}
// 取中位数
sort.Slice(offsets, func(i, j int) bool {
return offsets[i] < offsets[j]
})
median := offsets[len(offsets)/2]
ts := &TimeSync{Offset: median, Source: api.Name}
log.Printf("[时间同步] ✅ 使用 %s, 偏移量: %v (%d 次有效采样)",
api.Name, median, len(offsets))
return ts, nil
}
// 所有时间源都失败,降级到本地时间
log.Println("[时间同步] ⚠️ 所有时间源均失败,降级使用本地时间 (偏移=0)")
return &TimeSync{Offset: 0, Source: "本地时间(降级)"}, nil
}
// fetchOffset 根据 API 类型获取单次时间偏移
func fetchOffset(api config.TimeAPIConfig) (time.Duration, error) {
client := &http.Client{Timeout: 5 * time.Second}
localBefore := time.Now()
req, err := http.NewRequest("GET", api.URL, nil)
if err != nil {
return 0, fmt.Errorf("创建请求失败: %w", err)
}
// 模拟浏览器请求头
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
resp, err := client.Do(req)
if err != nil {
return 0, fmt.Errorf("请求失败: %w", err)
}
defer resp.Body.Close()
localAfter := time.Now()
rtt := localAfter.Sub(localBefore)
localEstimate := localBefore.Add(rtt / 2)
var serverTime time.Time
switch api.Type {
case config.TimeAPITypeTaobaoH5:
serverTime, err = parseTaobaoH5(resp)
case config.TimeAPITypeSuning:
serverTime, err = parseSuning(resp)
case config.TimeAPITypeHTTPHeader:
serverTime, err = parseHTTPDate(resp)
default:
return 0, fmt.Errorf("未知 API 类型")
}
if err != nil {
return 0, err
}
offset := serverTime.Sub(localEstimate)
log.Printf("[时间同步] RTT: %v, 服务器时间: %s", rtt, serverTime.Format("15:04:05.000"))
return offset, nil
}
// parseTaobaoH5 解析淘宝 H5 API 响应
func parseTaobaoH5(resp *http.Response) (time.Time, error) {
body, err := io.ReadAll(resp.Body)
if err != nil {
return time.Time{}, fmt.Errorf("读取响应失败: %w", err)
}
// 淘宝 H5 API 可能返回 JSONP需要提取 JSON 部分
bodyStr := string(body)
jsonStr := bodyStr
if idx := strings.Index(bodyStr, "("); idx != -1 {
jsonStr = bodyStr[idx+1:]
if endIdx := strings.LastIndex(jsonStr, ")"); endIdx != -1 {
jsonStr = jsonStr[:endIdx]
}
}
var result taobaoH5Resp
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
return time.Time{}, fmt.Errorf("解析 JSON 失败: %w (body: %s)", err, bodyStr[:min(len(bodyStr), 200)])
}
if result.Data.T == "" {
return time.Time{}, fmt.Errorf("时间戳为空 (body: %s)", bodyStr[:min(len(bodyStr), 200)])
}
ms, err := strconv.ParseInt(result.Data.T, 10, 64)
if err != nil {
return time.Time{}, fmt.Errorf("解析时间戳失败: %w", err)
}
return time.UnixMilli(ms), nil
}
// parseSuning 解析苏宁时间 API 响应
func parseSuning(resp *http.Response) (time.Time, error) {
body, err := io.ReadAll(resp.Body)
if err != nil {
return time.Time{}, fmt.Errorf("读取响应失败: %w", err)
}
var result suningTimeResp
if err := json.Unmarshal(body, &result); err != nil {
return time.Time{}, fmt.Errorf("解析 JSON 失败: %w", err)
}
if result.SysTime2 == "" {
return time.Time{}, fmt.Errorf("时间字段为空")
}
loc, _ := time.LoadLocation("Asia/Shanghai")
t, err := time.ParseInLocation("2006-01-02 15:04:05", result.SysTime2, loc)
if err != nil {
return time.Time{}, fmt.Errorf("解析时间失败: %w", err)
}
return t, nil
}
// parseHTTPDate 从 HTTP 响应头 Date 字段解析服务器时间
func parseHTTPDate(resp *http.Response) (time.Time, error) {
dateStr := resp.Header.Get("Date")
if dateStr == "" {
return time.Time{}, fmt.Errorf("响应头无 Date 字段")
}
// HTTP Date 格式: Thu, 09 Feb 2026 06:30:00 GMT
t, err := time.Parse(time.RFC1123, dateStr)
if err != nil {
// 尝试备用格式
t, err = time.Parse(time.RFC1123Z, dateStr)
if err != nil {
return time.Time{}, fmt.Errorf("解析 Date 头失败: %w (值: %s)", err, dateStr)
}
}
return t, nil
}
// Now 返回校准后的当前时间
func (ts *TimeSync) Now() time.Time {
return time.Now().Add(ts.Offset)
}
// UntilTarget 计算距离目标时间还有多久
func (ts *TimeSync) UntilTarget(target time.Time) time.Duration {
return target.Sub(ts.Now())
}