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()) }