2026-06-11 13:33:44 +08:00

149 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"
taskservice "hyapp/services/activity-service/internal/service/task"
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
task *taskservice.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, taskSvc *taskservice.Service, roomRewardSvc ...*roomturnoverrewardservice.Service) *BroadcastServer {
server := &BroadcastServer{svc: svc}
server.growth = growthSvc
server.weeklyStar = weeklyStarSvc
server.task = taskSvc
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.task != nil {
if _, err := s.task.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
}