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" ) // BroadcastServer 把 IM 播报能力暴露为内部 gRPC。 // 外部 HTTP 不直接调用这里;gateway/cron/room-service 只能通过 protobuf 契约进入 activity-service。 type BroadcastServer struct { activityv1.UnimplementedBroadcastServiceServer activityv1.UnimplementedRoomEventConsumerServiceServer svc *broadcastservice.Service growth *growthservice.Service } // NewBroadcastServer 创建播报和 room event 消费共用的 gRPC adapter。 func NewBroadcastServer(svc *broadcastservice.Service, growthSvc ...*growthservice.Service) *BroadcastServer { server := &BroadcastServer{svc: svc} if len(growthSvc) > 0 { server.growth = growthSvc[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 幂等消费。 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) } } return &activityv1.ConsumeRoomEventResponse{ EventId: result.EventID, Status: result.Status, BroadcastEventId: result.BroadcastEventID, BroadcastCreated: result.BroadcastCreated, }, nil }