package grpc import ( "context" "time" userv1 "hyapp.local/api/proto/user/v1" "hyapp/pkg/xerr" userdomain "hyapp/services/user-service/internal/domain/user" authservice "hyapp/services/user-service/internal/service/auth" mictimeservice "hyapp/services/user-service/internal/service/mictime" userservice "hyapp/services/user-service/internal/service/user" ) // ProcessLoginIPRiskBatch 处理一批登录后 IP 风控任务,任务 claim 和 session revoke 仍在 user-service 内完成。 func (s *Server) ProcessLoginIPRiskBatch(ctx context.Context, req *userv1.CronBatchRequest) (*userv1.CronBatchResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) if s.authSvc == nil { return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "auth service is not configured")) } processed, err := s.authSvc.ProcessLoginIPRiskBatch(ctx, authservice.LoginIPRiskWorkerOptions{ WorkerID: req.GetWorkerId(), LockTTL: durationFromMillis(req.GetLockTtlMs()), BatchSize: int(req.GetBatchSize()), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.CronBatchResponse{ ClaimedCount: int32(processed), ProcessedCount: int32(processed), SuccessCount: int32(processed), HasMore: processed >= normalizedBatchSize(req.GetBatchSize(), 100), }, nil } // ProcessRegionRebuildBatch 处理一个区域重算分页任务,按 app_code 作用域 claim。 func (s *Server) ProcessRegionRebuildBatch(ctx context.Context, req *userv1.CronBatchRequest) (*userv1.CronBatchResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) if s.userSvc == nil { return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "user service is not configured")) } result, err := s.userSvc.ProcessNextRegionRebuildTask(ctx, userservice.RegionRebuildWorkerOptions{ WorkerID: req.GetWorkerId(), LockTTL: durationFromMillis(req.GetLockTtlMs()), BatchSize: int(req.GetBatchSize()), }) if err != nil { return nil, xerr.ToGRPCError(err) } if result.NoTask { return &userv1.CronBatchResponse{}, nil } return &userv1.CronBatchResponse{ ClaimedCount: 1, ProcessedCount: int32(result.ProcessedUsers), SuccessCount: 1, HasMore: result.Status == userdomain.RegionRebuildTaskStatusPending, }, nil } // CompensateMicOpenSessions 关闭异常长时间未收到 down 事件的用户麦位统计 session,不反写 Room Cell。 func (s *Server) CompensateMicOpenSessions(ctx context.Context, req *userv1.CronBatchRequest) (*userv1.CronBatchResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) if s.micTimeSvc == nil { return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "mic time service is not configured")) } result, err := s.micTimeSvc.ProcessOpenSessionCompensationBatch(ctx, mictimeservice.CompensationWorkerOptions{ WorkerID: req.GetWorkerId(), BatchSize: int(req.GetBatchSize()), PendingPublishMaxAge: durationFromMillis(req.GetPendingPublishMaxAgeMs()), PublishingSessionMaxAge: durationFromMillis(req.GetPublishingSessionMaxAgeMs()), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.CronBatchResponse{ ClaimedCount: int32(result.ClosedCount), ProcessedCount: int32(result.ClosedCount), SuccessCount: int32(result.ClosedCount), HasMore: result.ClosedCount >= normalizedBatchSize(req.GetBatchSize(), 100), }, nil } // ExpireManagerUserBlocks 释放到期的经理封禁记录;真正是否恢复 users.status 由 user-service 内部封禁来源判断。 func (s *Server) ExpireManagerUserBlocks(ctx context.Context, req *userv1.CronBatchRequest) (*userv1.CronBatchResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) if s.userSvc == nil { return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "user service is not configured")) } processed, err := s.userSvc.ExpireManagerUserBlocks(ctx, req.GetBatchSize()) if err != nil { return nil, xerr.ToGRPCError(err) } return &userv1.CronBatchResponse{ ClaimedCount: int32(processed), ProcessedCount: int32(processed), SuccessCount: int32(processed), HasMore: processed >= normalizedBatchSize(req.GetBatchSize(), 100), }, nil } // RefreshCPIntimacyLeaderboard 重建 CP 亲密值排行榜 Redis zset;cron-service 只负责调度和租约。 func (s *Server) RefreshCPIntimacyLeaderboard(ctx context.Context, req *userv1.CronBatchRequest) (*userv1.CronBatchResponse, error) { // cron-service 每个 app_code 单独触发一次;这里把 app_code 放进 context 后,MySQL 查询和 Redis key 都会按租户隔离。 ctx = contextWithApp(ctx, req.GetMeta()) if s.cpSvc == nil { return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "cp service is not configured")) } // batch_size 在这个任务里不是“继续翻页”的游标,只是本轮榜单最大行数;service 会全量替换 Redis 读模型。 processed, err := s.cpSvc.RefreshIntimacyLeaderboard(ctx, int(req.GetBatchSize())) if err != nil { return nil, xerr.ToGRPCError(err) } // 刷新是一次性快照任务,没有 has_more 续跑语义;下一次 5 分钟调度会重新生成完整榜单。 return &userv1.CronBatchResponse{ ClaimedCount: int32(processed), ProcessedCount: int32(processed), SuccessCount: int32(processed), HasMore: false, }, nil } func durationFromMillis(value int64) time.Duration { if value <= 0 { return 0 } return time.Duration(value) * time.Millisecond } func normalizedBatchSize(value int32, fallback int) int { if value <= 0 { return fallback } return int(value) }