数据引擎重构
This commit is contained in:
+274
-92
@@ -13,8 +13,10 @@ import (
|
||||
dao "dataengine/dao/copydata"
|
||||
taskDto "dataengine/model/dto/copydata"
|
||||
entity "dataengine/model/entity/dict"
|
||||
"dataengine/utils"
|
||||
|
||||
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/os/grpool"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -46,6 +48,45 @@ type RecursiveConfig struct {
|
||||
TargetParam string `json:"target_param"`
|
||||
}
|
||||
|
||||
// isInterfaceDueForFullSync 检查单个接口是否该做全量同步了
|
||||
// 即使调用方要求增量,如果距上次全量超过阈值,自动升级为全量
|
||||
func isInterfaceDueForFullSync(ctx context.Context, platformCode, interfaceCode string) bool {
|
||||
interval := GetFullSyncIntervalHours(ctx)
|
||||
if interval <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
var lastFullSync int64
|
||||
// 使用 gfdb.Raw 确保 COALESCE 表达式不被框架改写
|
||||
record, err := gfdb.DB(ctx).Model(ctx, consts.SyncTrackerTable).
|
||||
Where("platform_code", platformCode).
|
||||
Where("interface_code", interfaceCode).
|
||||
Value("COALESCE(last_full_sync_time, 0)")
|
||||
if err != nil {
|
||||
logrus.Warnf("[%s/%s] 查询 last_full_sync_time 失败: %v,回退增量", platformCode, interfaceCode, err)
|
||||
return false
|
||||
}
|
||||
if record != nil {
|
||||
lastFullSync = record.Int64()
|
||||
}
|
||||
|
||||
logrus.Infof("[%s/%s] 读取到 last_full_sync_time=%d", platformCode, interfaceCode, lastFullSync)
|
||||
|
||||
if lastFullSync == 0 {
|
||||
logrus.Infof("[%s/%s] 从未全量,自动升级为全量同步", platformCode, interfaceCode)
|
||||
return true
|
||||
}
|
||||
|
||||
elapsed := time.Now().Unix() - lastFullSync
|
||||
if elapsed > int64(interval)*3600 {
|
||||
logrus.Infof("[%s/%s] 距上次全量 %d 小时(阈值 %d 小时),自动升级为全量同步",
|
||||
platformCode, interfaceCode, elapsed/3600, interval)
|
||||
return true
|
||||
}
|
||||
logrus.Debugf("[%s/%s] 距上次全量 %d 小时,未到阈值 %d 小时,执行增量", platformCode, interfaceCode, elapsed/3600, interval)
|
||||
return false
|
||||
}
|
||||
|
||||
// SyncByConfig 执行同步
|
||||
func SyncByConfig(ctx context.Context, platformCode, interfaceCode string, isFullSync bool) (*SyncResult, error) {
|
||||
// 创建超时 context 防止单次同步卡死
|
||||
@@ -92,21 +133,38 @@ func SyncByConfig(ctx context.Context, platformCode, interfaceCode string, isFul
|
||||
return nil, fmt.Errorf("建表失败: %w", err)
|
||||
}
|
||||
|
||||
// 先读取已有的 last_sync_time,供 markSyncRunning 保留(防止全量覆盖为0)
|
||||
existingLastSync := getLastSyncTime(ctx, platformCode, interfaceCode)
|
||||
|
||||
// 即使调用方要求增量,如果距上次全量超过阈值,自动升级为全量
|
||||
if !isFullSync {
|
||||
isFullSync = isInterfaceDueForFullSync(ctx, platformCode, interfaceCode)
|
||||
}
|
||||
|
||||
// 检查上次同步状态(在标记 running 之前检查)
|
||||
prevStatus := getSyncStatus(ctx, platformCode, interfaceCode)
|
||||
lastSyncTime := int64(0)
|
||||
if !isFullSync {
|
||||
lastSyncTime = getLastSyncTime(ctx, platformCode, interfaceCode)
|
||||
lastSyncTime = existingLastSync
|
||||
}
|
||||
if prevStatus == "running" {
|
||||
logrus.Warnf("检测到上次同步异常中断 [%s/%s],将重新全量同步", platformCode, interfaceCode)
|
||||
lastSyncTime = 0
|
||||
if lastSyncTime <= 0 {
|
||||
logrus.Warnf("检测到上次同步异常中断 [%s/%s],无有效同步点,执行全量", platformCode, interfaceCode)
|
||||
lastSyncTime = 0
|
||||
} else {
|
||||
logrus.Infof("检测到上次同步异常中断 [%s/%s],数据已落库,从同步点 %d 恢复增量", platformCode, interfaceCode, lastSyncTime)
|
||||
}
|
||||
}
|
||||
|
||||
// 标记同步开始(保留 last_sync_time 不变,状态设为 running)
|
||||
markSyncRunning(ctx, platformCode, interfaceCode, lastSyncTime)
|
||||
// 标记同步开始(不写入 last_sync_time,由 updateSyncTime 独家管理)
|
||||
markSyncRunning(ctx, platformCode, interfaceCode)
|
||||
|
||||
api := NewApiClient(platform)
|
||||
api.SetTokenRefreshFunc(RefreshTencentToken)
|
||||
// 支持 body_type: "json" 配置(如腾讯广告 POST 接口需要 JSON body 而非 form-encoded)
|
||||
if bt, ok := iface.RequestConfig["body_type"].(string); ok {
|
||||
api.SetBodyType(bt)
|
||||
}
|
||||
defer api.Close()
|
||||
|
||||
prefetch := parsePrefetchConfig(iface.RequestConfig)
|
||||
@@ -202,10 +260,10 @@ func syncSingleAPI(ctx context.Context, api *ApiClient, platform *PlatformConfig
|
||||
inQuery := paramsInQuery(iface)
|
||||
method := string(iface.Method)
|
||||
|
||||
// 游标参数名
|
||||
// 游标参数名(与 page_param 分开,腾讯游标模式使用独立的 cursor 参数)
|
||||
cursorParam := "cursor"
|
||||
if p, ok := iface.RequestConfig["page_param"].(string); ok && p != "" {
|
||||
cursorParam = p
|
||||
if cp, ok := iface.RequestConfig["cursor_param"].(string); ok && cp != "" {
|
||||
cursorParam = cp
|
||||
}
|
||||
cursorMode := isCursorPagination(iface)
|
||||
|
||||
@@ -314,11 +372,14 @@ func syncSingleAPI(ctx context.Context, api *ApiClient, platform *PlatformConfig
|
||||
}
|
||||
}
|
||||
|
||||
if globalMaxTime <= 0 {
|
||||
globalMaxTime = time.Now().Unix()
|
||||
if maxTime > globalMaxTime {
|
||||
globalMaxTime = maxTime
|
||||
}
|
||||
}
|
||||
updateSyncTime(ctx, platform.PlatformCode, iface.Code, globalMaxTime)
|
||||
if globalMaxTime <= 0 {
|
||||
globalMaxTime = time.Now().Unix()
|
||||
}
|
||||
updateSyncTime(ctx, platform.PlatformCode, iface.Code, globalMaxTime, taskType)
|
||||
|
||||
result.Duration = fmt.Sprintf("%.1fs", time.Since(start).Seconds())
|
||||
logrus.Infof("同步完成 - 表:%s, %d条, 写入%d条, 耗时%s", td.TableName, result.TotalRows, result.InsertedRows, result.Duration)
|
||||
@@ -401,7 +462,14 @@ func syncWithPrefetch(ctx context.Context, api *ApiClient, platform *PlatformCon
|
||||
if cp, ok := prefetchIface.RequestConfig["cursor_pagination"].(bool); ok {
|
||||
prefetchIsCursor = cp
|
||||
}
|
||||
if p, ok := prefetchIface.RequestConfig["page_param"].(string); ok && p != "" {
|
||||
// 游标模式使用独立的 cursor_param,不与 page_param 共用
|
||||
if prefetchIsCursor {
|
||||
if cp, ok := prefetchIface.RequestConfig["cursor_param"].(string); ok && cp != "" {
|
||||
prefetchPageParam = cp
|
||||
} else {
|
||||
prefetchPageParam = "cursor"
|
||||
}
|
||||
} else if p, ok := prefetchIface.RequestConfig["page_param"].(string); ok && p != "" {
|
||||
prefetchPageParam = p
|
||||
}
|
||||
}
|
||||
@@ -498,68 +566,94 @@ func syncWithPrefetch(ctx context.Context, api *ApiClient, platform *PlatformCon
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
} else {
|
||||
// ----- 常规分页预取 -----
|
||||
firstExtra := make(map[string]interface{})
|
||||
if prefetchIsCursor {
|
||||
// 支持 initial_cursor 配置,如果没有则使用空字符串
|
||||
if icv, ok := prefetchReqIface.RequestConfig["initial_cursor"]; ok {
|
||||
firstExtra[prefetchPageParam] = icv
|
||||
localLoaded := false
|
||||
// 优先从本地表读取预取数据,避免每次都调用 API
|
||||
localTable := ""
|
||||
if prefetchIface != nil && prefetchIface.TableDefinition != nil {
|
||||
if td, err := ParseTableDefinition(prefetchIface.TableDefinition); err == nil && td.TableName != "" {
|
||||
localTable = td.TableName
|
||||
}
|
||||
}
|
||||
if localTable != "" && prefetch.ValueField != "" {
|
||||
result, err := gfdb.DB(ctx).Model(ctx, localTable).
|
||||
Fields(prefetch.ValueField).
|
||||
All()
|
||||
if err != nil {
|
||||
logrus.Warnf("从本地表 %s 读取预取数据失败: %v", localTable, err)
|
||||
} else if result.Len() > 0 {
|
||||
logrus.Infof("从本地表 %s 读取到 %d 个预取实体", localTable, result.Len())
|
||||
for _, row := range result {
|
||||
if v := row[prefetch.ValueField]; v != nil {
|
||||
allEntities = append(allEntities, v.Val())
|
||||
}
|
||||
}
|
||||
localLoaded = true
|
||||
} else {
|
||||
firstExtra[prefetchPageParam] = ""
|
||||
logrus.Infof("本地表 %s 无数据,回退 API 预取", localTable)
|
||||
}
|
||||
}
|
||||
body := buildReqBody(ctx, prefetchReqIface, 1, prefetchPageSize, lastSyncTime, firstExtra)
|
||||
logrus.Debugf("预取请求 URL: %s, Method: %s, Body: %+v", prefetch.URL, prefetchMethod, body)
|
||||
resp, err := api.Request(ctx, prefetchMethod, prefetch.URL, body, prefetchInQuery)
|
||||
if err != nil {
|
||||
recordFailure(ctx, platform.PlatformCode, iface.Code, taskType, fmt.Sprintf("预取第一页请求失败: %v", err))
|
||||
return nil, fmt.Errorf("预取第一页失败: %w", err)
|
||||
}
|
||||
|
||||
rows, prefetchTotalPages, _, nextCursor, err := parseRespExt(resp.Body, prefetchRespCfg)
|
||||
if err != nil {
|
||||
recordFailure(ctx, platform.PlatformCode, iface.Code, taskType, fmt.Sprintf("解析预取响应失败: %v", err))
|
||||
return nil, fmt.Errorf("解析预取响应失败: %w", err)
|
||||
}
|
||||
collectPrefetchEntities(rows, prefetch, &allEntities, &allRows)
|
||||
|
||||
if prefetchIsCursor {
|
||||
for nextCursor != "" && nextCursor != "nomore" {
|
||||
body := buildReqBody(ctx, prefetchReqIface, 1, prefetchPageSize, lastSyncTime, map[string]interface{}{
|
||||
prefetchPageParam: nextCursor,
|
||||
})
|
||||
resp, err := api.Request(ctx, prefetchMethod, prefetch.URL, body, prefetchInQuery)
|
||||
if err != nil {
|
||||
logrus.Errorf("预取游标 %s 请求失败: %v", nextCursor, err)
|
||||
break
|
||||
if !localLoaded {
|
||||
// ----- 常规分页预取 -----
|
||||
firstExtra := make(map[string]interface{})
|
||||
if prefetchIsCursor {
|
||||
if icv, ok := prefetchReqIface.RequestConfig["initial_cursor"]; ok {
|
||||
firstExtra[prefetchPageParam] = icv
|
||||
}
|
||||
rows, _, _, nc, pe := parseRespExt(resp.Body, prefetchRespCfg)
|
||||
if pe != nil {
|
||||
logrus.Errorf("预取游标 %s 解析失败: %v", nextCursor, pe)
|
||||
break
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
break
|
||||
}
|
||||
nextCursor = nc
|
||||
collectPrefetchEntities(rows, prefetch, &allEntities, &allRows)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// 没有 initial_cursor 时不覆盖 page 参数,保持默认 page=1
|
||||
}
|
||||
} else {
|
||||
for page := 2; page <= prefetchTotalPages; page++ {
|
||||
body := buildReqBody(ctx, prefetchReqIface, page, prefetchPageSize, lastSyncTime, nil)
|
||||
resp, err := api.Request(ctx, prefetchMethod, prefetch.URL, body, prefetchInQuery)
|
||||
if err != nil {
|
||||
logrus.Errorf("预取第 %d 页请求失败: %v", page, err)
|
||||
continue
|
||||
body := buildReqBody(ctx, prefetchReqIface, 1, prefetchPageSize, lastSyncTime, firstExtra)
|
||||
logrus.Debugf("预取请求 URL: %s, Method: %s, Body: %+v", prefetch.URL, prefetchMethod, body)
|
||||
resp, err := api.Request(ctx, prefetchMethod, prefetch.URL, body, prefetchInQuery)
|
||||
if err != nil {
|
||||
recordFailure(ctx, platform.PlatformCode, iface.Code, taskType, fmt.Sprintf("预取第一页请求失败: %v", err))
|
||||
return nil, fmt.Errorf("预取第一页失败: %w", err)
|
||||
}
|
||||
|
||||
rows, prefetchTotalPages, _, nextCursor, err := parseRespExt(resp.Body, prefetchRespCfg)
|
||||
if err != nil {
|
||||
recordFailure(ctx, platform.PlatformCode, iface.Code, taskType, fmt.Sprintf("解析预取响应失败: %v", err))
|
||||
return nil, fmt.Errorf("解析预取响应失败: %w", err)
|
||||
}
|
||||
collectPrefetchEntities(rows, prefetch, &allEntities, &allRows)
|
||||
|
||||
if prefetchIsCursor {
|
||||
for nextCursor != "" && nextCursor != "nomore" {
|
||||
body := buildReqBody(ctx, prefetchReqIface, 1, prefetchPageSize, lastSyncTime, map[string]interface{}{
|
||||
prefetchPageParam: nextCursor,
|
||||
})
|
||||
resp, err := api.Request(ctx, prefetchMethod, prefetch.URL, body, prefetchInQuery)
|
||||
if err != nil {
|
||||
logrus.Errorf("预取游标 %s 请求失败: %v", nextCursor, err)
|
||||
break
|
||||
}
|
||||
rows, _, _, nc, pe := parseRespExt(resp.Body, prefetchRespCfg)
|
||||
if pe != nil {
|
||||
logrus.Errorf("预取游标 %s 解析失败: %v", nextCursor, pe)
|
||||
break
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
break
|
||||
}
|
||||
nextCursor = nc
|
||||
collectPrefetchEntities(rows, prefetch, &allEntities, &allRows)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
rows, _, _, _, pe := parseRespExt(resp.Body, prefetchRespCfg)
|
||||
if pe != nil {
|
||||
logrus.Errorf("预取第 %d 页解析失败: %v", page, pe)
|
||||
continue
|
||||
} else {
|
||||
for page := 2; page <= prefetchTotalPages; page++ {
|
||||
body := buildReqBody(ctx, prefetchReqIface, page, prefetchPageSize, lastSyncTime, nil)
|
||||
resp, err := api.Request(ctx, prefetchMethod, prefetch.URL, body, prefetchInQuery)
|
||||
if err != nil {
|
||||
logrus.Errorf("预取第 %d 页请求失败: %v", page, err)
|
||||
continue
|
||||
}
|
||||
rows, _, _, _, pe := parseRespExt(resp.Body, prefetchRespCfg)
|
||||
if pe != nil {
|
||||
logrus.Errorf("预取第 %d 页解析失败: %v", page, pe)
|
||||
continue
|
||||
}
|
||||
collectPrefetchEntities(rows, prefetch, &allEntities, &allRows)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
collectPrefetchEntities(rows, prefetch, &allEntities, &allRows)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -735,7 +829,7 @@ func syncWithPrefetch(ctx context.Context, api *ApiClient, platform *PlatformCon
|
||||
if globalMaxTime <= 0 {
|
||||
globalMaxTime = time.Now().Unix()
|
||||
}
|
||||
updateSyncTime(ctx, platform.PlatformCode, iface.Code, globalMaxTime)
|
||||
updateSyncTime(ctx, platform.PlatformCode, iface.Code, globalMaxTime, taskType)
|
||||
|
||||
result.Duration = fmt.Sprintf("%.1fs", time.Since(start).Seconds())
|
||||
logrus.Infof("同步完成 - 表:%s, %d条, 写入%d条, 耗时%s", td.TableName, result.TotalRows, result.InsertedRows, result.Duration)
|
||||
@@ -814,7 +908,7 @@ func syncRecursive(ctx context.Context, api *ApiClient, platform *PlatformConfig
|
||||
}
|
||||
|
||||
inserted, _ := savePage(ctx, td, allRows)
|
||||
updateSyncTime(ctx, platform.PlatformCode, iface.Code, time.Now().Unix())
|
||||
updateSyncTime(ctx, platform.PlatformCode, iface.Code, time.Now().Unix(), "full")
|
||||
|
||||
result := &SyncResult{
|
||||
TableName: td.TableName,
|
||||
@@ -910,7 +1004,7 @@ func buildPrefetchParams(iface *entity.ApiInterface) map[string]interface{} {
|
||||
k == "cursor_pagination" || k == "time_field_mode" ||
|
||||
k == "recursive" || k == "max_recursive_depth" ||
|
||||
k == "initial_cursor" || k == "pagination_mode" ||
|
||||
k == "full_sync_start_time" || k == "row_inject" {
|
||||
k == "body_type" || k == "full_sync_start_time" || k == "row_inject" {
|
||||
continue
|
||||
}
|
||||
if k == pageParam || k == psParam {
|
||||
@@ -1058,7 +1152,7 @@ func buildReqBody(ctx context.Context, iface *entity.ApiInterface, page, pageSiz
|
||||
k == "body_wrapper_field" || k == "exclude_from_wrapper" ||
|
||||
k == "top_level_params" || k == "recursive" ||
|
||||
k == "max_recursive_depth" || k == "initial_cursor" ||
|
||||
k == "pagination_mode" || k == "full_sync_start_time" ||
|
||||
k == "body_type" || k == "full_sync_start_time" ||
|
||||
k == "row_inject" {
|
||||
continue
|
||||
}
|
||||
@@ -1109,6 +1203,9 @@ func buildReqBody(ctx context.Context, iface *entity.ApiInterface, page, pageSiz
|
||||
} else {
|
||||
timeMs = time.Now().Add(-time.Duration(GetDefaultLookbackDays(ctx)) * 24 * time.Hour).UnixMilli()
|
||||
}
|
||||
} else if timeMs < 1000000000000 {
|
||||
// lastSyncTime 存的是秒(已归一化),快手 API 需要毫秒
|
||||
timeMs = timeMs * 1000
|
||||
}
|
||||
// 仅在配置未指定 queryType 时设默认值,尊重配置
|
||||
if _, exists := body["queryType"]; !exists {
|
||||
@@ -1145,6 +1242,21 @@ func buildReqBody(ctx context.Context, iface *entity.ApiInterface, page, pageSiz
|
||||
} else {
|
||||
body["filtering"] = []interface{}{timeFilter}
|
||||
}
|
||||
} else {
|
||||
// 全量且没有 full_sync_start_time:用 default_lookback_days 兜底
|
||||
lookbackDays := GetDefaultLookbackDays(ctx)
|
||||
defaultStart := time.Now().Add(-time.Duration(lookbackDays) * 24 * time.Hour).Unix()
|
||||
logrus.Infof("全量同步使用默认回溯 %d 天,过滤时间戳: %d", lookbackDays, defaultStart)
|
||||
timeFilter := map[string]interface{}{
|
||||
"field": tf,
|
||||
"operator": "GREATER_EQUALS",
|
||||
"values": []interface{}{fmt.Sprintf("%d", defaultStart)},
|
||||
}
|
||||
if existing, ok := body["filtering"].([]interface{}); ok {
|
||||
body["filtering"] = append(existing, timeFilter)
|
||||
} else {
|
||||
body["filtering"] = []interface{}{timeFilter}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1386,8 +1498,11 @@ func savePage(ctx context.Context, td *TableDefinition, rows []map[string]interf
|
||||
return 0, nil
|
||||
}
|
||||
colSet := make(map[string]bool)
|
||||
for _, c := range td.Columns {
|
||||
colDefs := make(map[string]*ColumnDef)
|
||||
for i := range td.Columns {
|
||||
c := &td.Columns[i]
|
||||
colSet[c.Name] = true
|
||||
colDefs[c.Name] = c
|
||||
}
|
||||
var clean []map[string]interface{}
|
||||
for _, row := range rows {
|
||||
@@ -1397,6 +1512,14 @@ func savePage(ctx context.Context, td *TableDefinition, rows []map[string]interf
|
||||
c[k] = v
|
||||
}
|
||||
}
|
||||
// 填充有默认值的字段(API 响应中缺失时使用)
|
||||
for name, col := range colDefs {
|
||||
if col.DefaultValue != "" {
|
||||
if _, exists := c[name]; !exists {
|
||||
c[name] = col.DefaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
if r, ok := row["raw_data"]; ok {
|
||||
c["raw_data"] = r
|
||||
}
|
||||
@@ -1406,46 +1529,85 @@ func savePage(ctx context.Context, td *TableDefinition, rows []map[string]interf
|
||||
}
|
||||
|
||||
func getLastSyncTime(ctx context.Context, platformCode, interfaceCode string) int64 {
|
||||
var t int64
|
||||
gfdb.DB(ctx).Model(ctx, consts.SyncTrackerTable).
|
||||
Fields("last_sync_time").
|
||||
v, err := gfdb.DB(ctx).Model(ctx, consts.SyncTrackerTable).
|
||||
Where("platform_code", platformCode).
|
||||
Where("interface_code", interfaceCode).
|
||||
Scan(&t)
|
||||
return t
|
||||
Value("last_sync_time")
|
||||
if err != nil || v == nil {
|
||||
return 0
|
||||
}
|
||||
return v.Int64()
|
||||
}
|
||||
|
||||
func getSyncStatus(ctx context.Context, platformCode, interfaceCode string) string {
|
||||
var s string
|
||||
gfdb.DB(ctx).Model(ctx, consts.SyncTrackerTable).
|
||||
Fields("sync_status").
|
||||
v, err := gfdb.DB(ctx).Model(ctx, consts.SyncTrackerTable).
|
||||
Where("platform_code", platformCode).
|
||||
Where("interface_code", interfaceCode).
|
||||
Scan(&s)
|
||||
return s
|
||||
Value("sync_status")
|
||||
if err != nil || v == nil {
|
||||
return ""
|
||||
}
|
||||
return v.String()
|
||||
}
|
||||
|
||||
func markSyncRunning(ctx context.Context, platformCode, interfaceCode string, lastSyncTime int64) {
|
||||
func getSyncCount(ctx context.Context, platformCode, interfaceCode string) int64 {
|
||||
v, err := gfdb.DB(ctx).Model(ctx, consts.SyncTrackerTable).
|
||||
Where("platform_code", platformCode).
|
||||
Where("interface_code", interfaceCode).
|
||||
Value("COALESCE(sync_count, 0)")
|
||||
if err != nil || v == nil {
|
||||
return 0
|
||||
}
|
||||
return v.Int64()
|
||||
}
|
||||
|
||||
func markSyncRunning(ctx context.Context, platformCode, interfaceCode string) {
|
||||
tenantId := utils.GetCurrentTenantId(ctx)
|
||||
gfdb.DB(ctx).Model(ctx, consts.SyncTrackerTable).
|
||||
Data(map[string]interface{}{
|
||||
"platform_code": platformCode,
|
||||
"interface_code": interfaceCode,
|
||||
"last_sync_time": lastSyncTime,
|
||||
"sync_status": "running",
|
||||
"tenant_id": tenantId,
|
||||
}).
|
||||
OnConflict("platform_code", "interface_code").
|
||||
Save()
|
||||
}
|
||||
|
||||
func updateSyncTime(ctx context.Context, platformCode, interfaceCode string, t int64) {
|
||||
// normalizeSyncTimestamp 将同步时间戳统一归一化到秒
|
||||
// 外部 API 响应中的时间戳可能是秒或毫秒,统一存为秒
|
||||
func normalizeSyncTimestamp(ts int64) int64 {
|
||||
if ts > 1000000000000 { // > 1e12 显然是毫秒
|
||||
return ts / 1000
|
||||
}
|
||||
return ts
|
||||
}
|
||||
|
||||
// updateSyncTime 更新同步跟踪记录
|
||||
// syncType: "full" / "incremental"
|
||||
func updateSyncTime(ctx context.Context, platformCode, interfaceCode string, t int64, syncType string) {
|
||||
t = normalizeSyncTimestamp(t)
|
||||
tenantId := utils.GetCurrentTenantId(ctx)
|
||||
|
||||
// 先读取当前的 sync_count
|
||||
currentCount := getSyncCount(ctx, platformCode, interfaceCode)
|
||||
|
||||
data := gdb.Map{
|
||||
"platform_code": platformCode,
|
||||
"interface_code": interfaceCode,
|
||||
"last_sync_time": t,
|
||||
"last_sync_at": time.Now(),
|
||||
"sync_status": "success",
|
||||
"last_sync_type": syncType,
|
||||
"sync_count": currentCount + 1,
|
||||
"tenant_id": tenantId,
|
||||
}
|
||||
if syncType == "full" {
|
||||
data["last_full_sync_time"] = time.Now().Unix() // 用当前时间,不是数据时间戳
|
||||
}
|
||||
|
||||
gfdb.DB(ctx).Model(ctx, consts.SyncTrackerTable).
|
||||
Data(map[string]interface{}{
|
||||
"platform_code": platformCode,
|
||||
"interface_code": interfaceCode,
|
||||
"last_sync_time": t,
|
||||
"last_sync_at": time.Now(),
|
||||
"sync_status": "success",
|
||||
}).
|
||||
Data(data).
|
||||
OnConflict("platform_code", "interface_code").
|
||||
Save()
|
||||
}
|
||||
@@ -1465,6 +1627,26 @@ func recordFailure(ctx context.Context, platformCode, interfaceCode, taskType, e
|
||||
})
|
||||
}
|
||||
|
||||
// isTokenExpiredError 判断错误是否因 token 过期导致
|
||||
func isTokenExpiredError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := err.Error()
|
||||
// 常见 token 过期标识(兼容各平台)
|
||||
tokenExpiredKeywords := []string{
|
||||
"TOKEN过期", "token过期", "token_expired", "Token过期",
|
||||
"result=28", // 快手
|
||||
"access_token", "token已失效", "token无效",
|
||||
}
|
||||
for _, kw := range tokenExpiredKeywords {
|
||||
if strings.Contains(msg, kw) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// findInterfaceByURL 在所有接口中查找匹配 URL 的接口
|
||||
func findInterfaceByURL(ifaces []entity.ApiInterface, url string) *entity.ApiInterface {
|
||||
for i := range ifaces {
|
||||
|
||||
Reference in New Issue
Block a user