Files
linkmaster-node/internal/timesync/sync.go
yoyo 7a104bbe42 feat: 更新时间同步逻辑,使用K780时间API替代中国时间API
- 修改时间同步服务,使用K780时间API获取北京时间,增强了API请求的错误处理和日志记录。
- 更新响应结构,解析新的时间戳格式,确保时间同步的准确性和稳定性。
2025-12-24 02:57:37 +08:00

222 lines
5.9 KiB
Go
Raw 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 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", &timestamp); 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
}