package grpc import ( "context" "time" activityv1 "hyapp.local/api/proto/activity/v1" "hyapp/pkg/appcode" "hyapp/pkg/xerr" broadcastservice "hyapp/services/activity-service/internal/service/broadcast" growthservice "hyapp/services/activity-service/internal/service/growth" roomturnoverrewardservice "hyapp/services/activity-service/internal/service/roomturnoverreward" weeklystarservice "hyapp/services/activity-service/internal/service/weeklystar" ) // BroadcastServer 把 IM 播报能力暴露为内部 gRPC。 // 外部 HTTP 不直接调用这里;gateway/cron/room-service 只能通过 protobuf 契约进入 activity-service。 type BroadcastServer struct { activityv1.UnimplementedBroadcastServiceServer activityv1.UnimplementedRoomEventConsumerServiceServer svc *broadcastservice.Service growth *growthservice.Service roomReward *roomturnoverrewardservice.Service weeklyStar *weeklystarservice.Service } // NewBroadcastServer 创建播报和 room event 消费共用的 gRPC adapter。 // room-service 的 outbox 事件只投递一次,但 activity-service 内部有多条独立投影: // 播报负责 IM 展示,growth 负责等级进度,weekly-star 负责指定礼物榜单,roomReward 负责房间流水。 // 这里把这些模块都挂到同一个 gRPC 入口,保证本地直连投递和 MQ 投递使用同一组业务副作用。 func NewBroadcastServer(svc *broadcastservice.Service, growthSvc *growthservice.Service, weeklyStarSvc *weeklystarservice.Service, roomRewardSvc ...*roomturnoverrewardservice.Service) *BroadcastServer { server := &BroadcastServer{svc: svc} server.growth = growthSvc server.weeklyStar = weeklyStarSvc if len(roomRewardSvc) > 0 { server.roomReward = roomRewardSvc[0] } return server } func (s *BroadcastServer) EnsureBroadcastGroups(ctx context.Context, req *activityv1.EnsureBroadcastGroupsRequest) (*activityv1.EnsureBroadcastGroupsResponse, error) { // app_code 只从 RequestMeta 进入 context,底层 repository 才能自动带上租户隔离条件。 ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) count, err := s.svc.EnsureBroadcastGroups(ctx) if err != nil { return nil, xerr.ToGRPCError(err) } return &activityv1.EnsureBroadcastGroupsResponse{EnsuredCount: int32(count)}, nil } func (s *BroadcastServer) PublishRegionBroadcast(ctx context.Context, req *activityv1.PublishRegionBroadcastRequest) (*activityv1.PublishBroadcastResponse, error) { // 区域播报是红包、贵重礼物等高频入口,gRPC 层只做字段搬运,策略留给 service。 ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) result, err := s.svc.PublishRegionBroadcast(ctx, broadcastservice.PublishInput{ EventID: req.GetEventId(), BroadcastType: req.GetBroadcastType(), RegionID: req.GetRegionId(), PayloadJSON: req.GetPayloadJson(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &activityv1.PublishBroadcastResponse{EventId: result.EventID, GroupId: result.GroupID, Status: result.Status, Created: result.Created}, nil } func (s *BroadcastServer) PublishGlobalBroadcast(ctx context.Context, req *activityv1.PublishGlobalBroadcastRequest) (*activityv1.PublishBroadcastResponse, error) { // 全局播报和区域播报共用返回结构,调用方通过 group_id/status/created 判断幂等结果。 ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) result, err := s.svc.PublishGlobalBroadcast(ctx, broadcastservice.PublishInput{ EventID: req.GetEventId(), BroadcastType: req.GetBroadcastType(), PayloadJSON: req.GetPayloadJson(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &activityv1.PublishBroadcastResponse{EventId: result.EventID, GroupId: result.GroupID, Status: result.Status, Created: result.Created}, nil } func (s *BroadcastServer) RemoveRegionBroadcastMember(ctx context.Context, req *activityv1.RemoveRegionBroadcastMemberRequest) (*activityv1.RemoveRegionBroadcastMemberResponse, error) { // 这里只删除 user_id 在旧区域群的成员关系,不删除区域群;群生命周期仍由 reconciler 管理。 ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) groupID, removed, err := s.svc.RemoveRegionBroadcastMember(ctx, req.GetUserId(), req.GetRegionId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &activityv1.RemoveRegionBroadcastMemberResponse{GroupId: groupID, Removed: removed}, nil } func (s *BroadcastServer) ProcessBroadcastOutboxBatch(ctx context.Context, req *activityv1.CronBatchRequest) (*activityv1.ProcessBroadcastOutboxBatchResponse, error) { // cron-service 可以显式触发一批投递;常驻 worker 也复用同一个 service 方法。 ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) result, err := s.svc.ProcessPendingBroadcasts(ctx, broadcastservice.WorkerOptions{ WorkerID: req.GetWorkerId(), BatchSize: int(req.GetBatchSize()), LockTTL: time.Duration(req.GetLockTtlMs()) * time.Millisecond, }) if err != nil { return nil, xerr.ToGRPCError(err) } return &activityv1.ProcessBroadcastOutboxBatchResponse{ ClaimedCount: int32(result.ClaimedCount), SuccessCount: int32(result.SuccessCount), FailureCount: int32(result.FailureCount), HasMore: result.HasMore, }, nil } func (s *BroadcastServer) ConsumeRoomEvent(ctx context.Context, req *activityv1.ConsumeRoomEventRequest) (*activityv1.ConsumeRoomEventResponse, error) { // room-service 只推送已提交的 outbox envelope;每个下游模块都以同一个 event_id 做自己的幂等表。 // 调用顺序保持和 MQ consumer 一致:先处理可见播报,再处理用户成长,再处理运营活动统计。 // 任一模块返回错误时直接失败,让 room-service/MQ 重试同一个 envelope,而不是吞掉部分副作用。 ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) result, err := s.svc.HandleRoomEvent(ctx, req.GetEnvelope()) if err != nil { return nil, xerr.ToGRPCError(err) } if s.growth != nil { if _, err := s.growth.HandleRoomEvent(ctx, req.GetEnvelope()); err != nil { return nil, xerr.ToGRPCError(err) } } if s.weeklyStar != nil { if _, err := s.weeklyStar.HandleRoomEvent(ctx, req.GetEnvelope()); err != nil { return nil, xerr.ToGRPCError(err) } } if s.roomReward != nil { if _, err := s.roomReward.HandleRoomEvent(ctx, req.GetEnvelope()); err != nil { return nil, xerr.ToGRPCError(err) } } return &activityv1.ConsumeRoomEventResponse{ EventId: result.EventID, Status: result.Status, BroadcastEventId: result.BroadcastEventID, BroadcastCreated: result.BroadcastCreated, }, nil }