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) }