package broadcast import ( "context" "log/slog" "time" "hyapp/pkg/logx" ) // RunWorker 先按需补齐播报群,再持续拉取持久化 outbox 投递腾讯 IM。 // 该 worker 只在 broadcast.enabled 且 tencent_im.enabled 时启动,本地默认关闭,防止开发环境误发真实 IM。 func (s *Service) RunWorker(ctx context.Context, options WorkerOptions) { options = normalizeWorkerOptions(options, s.cfg) if s.cfg.EnsureGroupsOnStartup { // 建群失败不退出进程:下一轮部署/定时任务可继续补偿,已经入队的消息仍保留在 MySQL。 s.reconcileBroadcastGroups(ctx, options) } ticker := time.NewTicker(options.PollInterval) defer ticker.Stop() lastReconcile := s.now() for { // 定时 reconcile 兜底新区域上线和启动时腾讯 REST 抖动:只跑一次的启动补偿失败后, // 缺失的区域群会让该区域全部播报静默 failed 沉底,不能等下一次部署才补群。 if s.cfg.EnsureGroupsInterval > 0 && s.now().Sub(lastReconcile) >= s.cfg.EnsureGroupsInterval { lastReconcile = s.now() s.reconcileBroadcastGroups(ctx, options) } if _, err := s.ProcessPendingBroadcasts(ctx, options); err != nil && ctx.Err() == nil { // 批处理失败只影响当前轮询,outbox 记录没有被删除,下一轮会按锁和重试状态重新处理。 logx.Error(ctx, "broadcast_outbox_batch_failed", err, slog.String("worker_id", options.WorkerID)) } select { case <-ctx.Done(): return case <-ticker.C: } } } func (s *Service) reconcileBroadcastGroups(ctx context.Context, options WorkerOptions) { if _, err := s.EnsureBroadcastGroups(ctx); err != nil && ctx.Err() == nil { logx.Error(ctx, "broadcast_group_reconcile_failed", err, slog.String("worker_id", options.WorkerID)) } }