第一版

This commit is contained in:
zyj
2026-02-09 16:05:56 +08:00
parent fcdc5b8f2d
commit 514d7f9b0e
11 changed files with 1137 additions and 365 deletions

216
utils/timesync.go Normal file
View File

@@ -0,0 +1,216 @@
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())
}