Files
data-engine/service/sync/sync_scheduler.go
T
2026-07-16 14:34:06 +08:00

202 lines
6.7 KiB
Go

package sync
import (
"context"
"time"
consts "dataengine/consts/public"
dao "dataengine/dao/dict"
dto "dataengine/model/dto/dict"
"gitea.redpowerfuture.com/red-future/common/beans"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/sirupsen/logrus"
)
// SyncRunItemResult 单次同步执行结果
type SyncRunItemResult struct {
PlatformCode string `json:"platformCode"`
InterfaceCode string `json:"interfaceCode"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
// StartAutoSync 启动自动同步(独立 goroutine,每次完成后等待 interval 再执行下一次)
func StartAutoSync(ctx context.Context) {
interval := GetSyncInterval(ctx)
logrus.Infof("自动同步调度器启动,间隔: %d 分钟(完成一次后开始计时)", interval)
for {
runAutoSync(ctx, false) // 后台调度不关心返回结果,错误已内部打日志
logrus.Infof("自动同步完成,等待 %d 分钟后执行下一次", interval)
time.Sleep(time.Duration(interval) * time.Minute)
}
}
// determineSyncMode 判断本次同步应该增量还是全量
// 返回 true=全量, false=增量
func determineSyncMode(ctx context.Context) bool {
interval := GetFullSyncIntervalHours(ctx)
if interval <= 0 {
return false // 自动全量关闭,永远增量
}
// 查询 sync_tracker 中最早的全量同步时间
v, err := gfdb.DB(ctx).Model(ctx, consts.SyncTrackerTable).
Value("COALESCE(MIN(last_full_sync_time), 0)")
if err != nil {
logrus.Warnf("查询全量同步时间失败,回退增量模式: %v", err)
return false
}
minFullSync := int64(0)
if v != nil {
minFullSync = v.Int64()
}
if minFullSync == 0 {
logrus.Info("检测到从未全量同步的接口,本次执行全量同步")
return true
}
elapsed := time.Now().Unix() - minFullSync
if elapsed > int64(interval)*3600 {
logrus.Infof("距上次全量已过 %d 小时(阈值 %d 小时),本次执行全量同步", elapsed/3600, interval)
return true
}
logrus.Debugf("距上次全量 %d 小时,未到阈值 %d 小时,执行增量同步", elapsed/3600, interval)
return false
}
func runAutoSync(ctx context.Context, forceFull bool) []SyncRunItemResult {
logrus.Info("=== 开始自动同步 ===")
// 判断本次同步模式(增量 / 全量)
isFullSync := forceFull || determineSyncMode(ctx)
logrus.Infof("本次同步模式: %s", map[bool]string{true: "全量", false: "增量"}[isFullSync])
// 从配置读取同步租户 ID(运维部署时配置)
tenantId := g.Cfg().MustGet(ctx, "sync.default_tenant_id", 1).Uint64()
// 注入用户上下文(ORM 框架需要用于租户隔离)
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: tenantId})
// 查询所有 ACTIVE 平台
platforms, _, err := dao.DatasourcePlatform.List(ctx, &dto.ListDatasourcePlatformReq{
Status: "ACTIVE",
}, tenantId)
if err != nil {
logrus.Errorf("查询平台列表失败: %v", err)
return nil
}
var results []SyncRunItemResult
for _, p := range platforms {
// 查询该平台下有 table_definition 的接口
interfaces, _, err := dao.ApiInterface.List(ctx, &dto.ListApiInterfaceReq{
PlatformId: p.Id,
Status: "active",
}, tenantId)
if err != nil {
logrus.Errorf("查询接口列表失败 [platform=%s]: %v", p.PlatformCode, err)
continue
}
for _, iface := range interfaces {
if iface.TableDefinition == nil || len(iface.TableDefinition) == 0 {
continue
}
logrus.Infof("自动同步: %s / %s", p.PlatformCode, iface.Code)
_, err := SyncByConfig(ctx, p.PlatformCode, iface.Code, isFullSync)
item := SyncRunItemResult{
PlatformCode: p.PlatformCode,
InterfaceCode: iface.Code,
}
if err != nil {
logrus.Errorf("自动同步失败 [%s/%s]: %v", p.PlatformCode, iface.Code, err)
item.Success = false
item.Error = err.Error()
results = append(results, item)
// token 过期是平台级别问题,该平台剩余接口继续请求只会重复失败,直接跳过
if isTokenExpiredError(err) {
logrus.Warnf("平台 [%s] token 已过期,跳过该平台剩余接口", p.PlatformCode)
break
}
} else {
item.Success = true
results = append(results, item)
}
}
}
logrus.Info("=== 自动同步完成 ===")
return results
}
// TriggerAllSync 手动触发全量同步(等价于 runAutoSync 的一次执行)
// 由 HTTP 端点调用,用于 PPGo_Job 调度。
// forceFull=true 强制全量,false 则由自动策略判断。
// 返回每个平台接口的执行结果列表。
func TriggerAllSync(ctx context.Context, forceFull bool) []SyncRunItemResult {
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: g.Cfg().MustGet(ctx, "sync.default_tenant_id", 1).Uint64()})
return runAutoSync(ctx, forceFull)
}
// RecoverInterruptedSyncs 服务启动时恢复异常中断的同步
// 扫描 sync_tracker 中 sync_status="running" 的接口,自动重新全量同步
// 适用于服务进程崩溃后重启的场景,不等 PPGo_Job 下轮调度
func RecoverInterruptedSyncs(ctx context.Context) {
type trackerItem struct {
PlatformCode string
InterfaceCode string
}
var items []trackerItem
err := gfdb.DB(ctx).Model(ctx, consts.SyncTrackerTable).
Fields("platform_code", "interface_code").
Where("sync_status", "running").
Scan(&items)
if err != nil {
logrus.Warnf("查询中断同步任务失败: %v", err)
return
}
if len(items) == 0 {
logrus.Info("启动恢复扫描:没有发现异常中断的同步任务")
return
}
tenantId := g.Cfg().MustGet(ctx, "sync.default_tenant_id", 1).Uint64()
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: tenantId})
logrus.Infof("启动恢复扫描:发现 %d 个异常中断的同步任务,开始恢复...", len(items))
for _, item := range items {
logrus.Infof("正在恢复中断的同步 [%s/%s]...", item.PlatformCode, item.InterfaceCode)
_, err := SyncByConfig(ctx, item.PlatformCode, item.InterfaceCode, true)
if err != nil {
logrus.Errorf("恢复同步失败 [%s/%s]: %v", item.PlatformCode, item.InterfaceCode, err)
} else {
logrus.Infof("恢复同步成功 [%s/%s]", item.PlatformCode, item.InterfaceCode)
}
}
}
// InitAndStartAutoSync 在 main 中调用:初始化配置后启动自动同步和补偿
func InitAndStartAutoSync(ctx context.Context) {
// 服务启动时恢复异常中断的同步(不等 PPGo_Job 调度)
RecoverInterruptedSyncs(ctx)
// 读取配置中的同步开关
enabled := g.Cfg().MustGet(ctx, "sync.auto_sync_enabled", false).Bool()
if enabled {
go StartAutoSync(ctx)
} else {
logrus.Info("自动同步已关闭")
}
// 补偿调度器独立启动,不受 auto_sync_enabled 控制
go StartCompensation(ctx)
}