package timesync import ( "context" "encoding/json" "fmt" "net/http" "sync" "time" "go.uber.org/zap" ) // TimeSync 时间同步器 type TimeSync struct { logger *zap.Logger stopCh chan struct{} beijingTZ *time.Location lastSyncTime time.Time lastSyncError error mu sync.RWMutex } // K780TimeAPIResponse K780时间API响应结构 type K780TimeAPIResponse struct { Success string `json:"success"` Msgid string `json:"msgid"` Msg string `json:"msg"` Result struct { Timestamp string `json:"timestamp"` // 时间戳(秒) TimestampMs string `json:"timestamp_ms"` // 时间戳(毫秒) Datetime1 string `json:"datetime_1"` // 日期时间格式1 Datetime2 string `json:"datetime_2"` // 日期时间格式2 } `json:"result"` } // NewTimeSync 创建时间同步器 func NewTimeSync(logger *zap.Logger) (*TimeSync, error) { // 加载北京时间时区 beijingTZ, err := time.LoadLocation("Asia/Shanghai") if err != nil { return nil, fmt.Errorf("加载时区失败: %w", err) } return &TimeSync{ logger: logger, stopCh: make(chan struct{}), beijingTZ: beijingTZ, }, nil } // syncTime 同步时间(从HTTP API获取北京时间) func (ts *TimeSync) syncTime() error { // 使用K780时间API apiURL := "https://sapi.k780.com/?app=life.time&appkey=10003&sign=b59bc3ef6191eb9f747dd4e83c99f2a4&format=json" client := &http.Client{ Timeout: 5 * time.Second, } // 创建请求,添加浏览器请求头 req, err := http.NewRequest("GET", apiURL, nil) if err != nil { ts.mu.Lock() ts.lastSyncError = err ts.mu.Unlock() return fmt.Errorf("创建请求失败: %w", err) } // 添加浏览器请求头,模拟浏览器访问 req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7") req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9") req.Header.Set("Cache-Control", "max-age=0") req.Header.Set("Connection", "keep-alive") req.Header.Set("Referer", "https://www.nowapi.com/") req.Header.Set("Sec-Fetch-Dest", "document") req.Header.Set("Sec-Fetch-Mode", "navigate") req.Header.Set("Sec-Fetch-Site", "cross-site") req.Header.Set("Sec-Fetch-User", "?1") req.Header.Set("Upgrade-Insecure-Requests", "1") req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36") req.Header.Set("sec-ch-ua", `"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"`) req.Header.Set("sec-ch-ua-mobile", "?0") req.Header.Set("sec-ch-ua-platform", `"macOS"`) resp, err := client.Do(req) if err != nil { ts.mu.Lock() ts.lastSyncError = err ts.mu.Unlock() return fmt.Errorf("API请求失败: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { err := fmt.Errorf("API返回状态码: %d", resp.StatusCode) ts.mu.Lock() ts.lastSyncError = err ts.mu.Unlock() return err } var result K780TimeAPIResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { ts.mu.Lock() ts.lastSyncError = err ts.mu.Unlock() return fmt.Errorf("解析API响应失败: %w", err) } // 检查返回状态 if result.Success != "1" { errMsg := fmt.Sprintf("API返回失败状态: success=%s", result.Success) if result.Msg != "" { errMsg += fmt.Sprintf(", msg=%s", result.Msg) } if result.Msgid != "" { errMsg += fmt.Sprintf(", msgid=%s", result.Msgid) } err := fmt.Errorf(errMsg) ts.mu.Lock() ts.lastSyncError = err ts.mu.Unlock() ts.logger.Warn("时间API返回失败", zap.String("success", result.Success), zap.String("msgid", result.Msgid), zap.String("msg", result.Msg)) return err } // 从result.timestamp字段解析时间戳字符串 if result.Result.Timestamp == "" { err := fmt.Errorf("API返回的时间戳为空") ts.mu.Lock() ts.lastSyncError = err ts.mu.Unlock() return err } // 解析时间戳字符串为int64 var timestamp int64 if _, err := fmt.Sscanf(result.Result.Timestamp, "%d", ×tamp); err != nil { err := fmt.Errorf("解析时间戳失败: %w", err) ts.mu.Lock() ts.lastSyncError = err ts.mu.Unlock() return err } // 使用时间戳转换为北京时间 beijingTime := time.Unix(timestamp, 0).In(ts.beijingTZ) // 计算时间差 localTime := time.Now() timeDiff := beijingTime.Sub(localTime) ts.mu.Lock() ts.lastSyncTime = beijingTime ts.lastSyncError = nil ts.mu.Unlock() ts.logger.Info("时间同步成功", zap.String("api", apiURL), zap.String("remote_time", beijingTime.Format("2006-01-02 15:04:05")), zap.String("local_time", localTime.Format("2006-01-02 15:04:05")), zap.Duration("time_diff", timeDiff), zap.Int64("timestamp", timestamp)) return nil } // Start 启动定期时间同步 func (ts *TimeSync) Start(ctx context.Context, interval time.Duration) { // 立即同步一次 if err := ts.syncTime(); err != nil { ts.logger.Warn("初始时间同步失败", zap.Error(err)) } // 定期同步 ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ts.stopCh: return case <-ticker.C: if err := ts.syncTime(); err != nil { ts.logger.Warn("时间同步失败", zap.Error(err)) } } } } // Stop 停止时间同步 func (ts *TimeSync) Stop() { close(ts.stopCh) } // GetBeijingTime 获取当前北京时间 func (ts *TimeSync) GetBeijingTime() time.Time { return time.Now().In(ts.beijingTZ) } // GetLocation 获取北京时区 func (ts *TimeSync) GetLocation() *time.Location { return ts.beijingTZ } // GetLastSyncTime 获取最后一次同步的时间 func (ts *TimeSync) GetLastSyncTime() time.Time { ts.mu.RLock() defer ts.mu.RUnlock() return ts.lastSyncTime } // GetLastSyncError 获取最后一次同步的错误 func (ts *TimeSync) GetLastSyncError() error { ts.mu.RLock() defer ts.mu.RUnlock() return ts.lastSyncError }