166 lines
6.0 KiB
Go
166 lines
6.0 KiB
Go
package roomnotice
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
|
"hyapp/pkg/logx"
|
|
"hyapp/pkg/tencentim"
|
|
)
|
|
|
|
// Repository 是 room notice 模块需要的持久化边界。
|
|
type Repository interface {
|
|
ClaimRoomKickEvents(ctx context.Context, workerID string, limit int, lockTTL time.Duration) ([]RoomKickEvent, error)
|
|
ClaimRoomKickEnvelope(ctx context.Context, workerID string, envelope *roomeventsv1.EventEnvelope, lockTTL time.Duration) (RoomKickEvent, bool, error)
|
|
MarkRoomKickDelivered(ctx context.Context, event RoomKickEvent, deliveredPayload []byte, nowMs int64) error
|
|
MarkRoomKickRetryable(ctx context.Context, event RoomKickEvent, retryCount int, nextRetryAtMS int64, lastErr string, nowMs int64) error
|
|
MarkRoomKickFailed(ctx context.Context, event RoomKickEvent, retryCount int, lastErr string, nowMs int64) error
|
|
}
|
|
|
|
// UserMessagePublisher 把房间私有 notice 投递到用户个人实时通道。
|
|
type UserMessagePublisher interface {
|
|
PublishUserCustomMessage(ctx context.Context, message tencentim.CustomUserMessage) error
|
|
}
|
|
|
|
// Config 保存 room notice 的进程级配置。
|
|
type Config struct {
|
|
NodeID string
|
|
}
|
|
|
|
// Service 消费 room_outbox 中需要私有通知的房间事实,不拥有房间状态。
|
|
type Service struct {
|
|
cfg Config
|
|
repository Repository
|
|
publisher UserMessagePublisher
|
|
}
|
|
|
|
// New 创建 room notice 服务。
|
|
func New(cfg Config, repository Repository, publisher UserMessagePublisher) *Service {
|
|
return &Service{cfg: cfg, repository: repository, publisher: publisher}
|
|
}
|
|
|
|
// ProcessRoomKickNotices 处理一批“被踢用户本人”的私有通知。
|
|
func (s *Service) ProcessRoomKickNotices(ctx context.Context, options RoomNoticeWorkerOptions) (int, error) {
|
|
options = normalizeRoomNoticeWorkerOptions(options, s.cfg.NodeID)
|
|
if s.repository == nil {
|
|
return 0, fmt.Errorf("notice repository is not configured")
|
|
}
|
|
if s.publisher == nil {
|
|
return 0, fmt.Errorf("notice publisher is not configured")
|
|
}
|
|
events, err := s.repository.ClaimRoomKickEvents(ctx, options.WorkerID, options.BatchSize, options.LockTTL)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
processed := 0
|
|
for _, event := range events {
|
|
if err := s.publishRoomKickEvent(ctx, event, options); err != nil {
|
|
return processed, err
|
|
}
|
|
processed++
|
|
}
|
|
return processed, nil
|
|
}
|
|
|
|
// ProcessRoomOutboxEnvelope handles one room_outbox MQ message. Unknown event
|
|
// types are acknowledged by returning handled=false; poison messages for known
|
|
// types still return an error so MQ can retry or dead-letter them.
|
|
func (s *Service) ProcessRoomOutboxEnvelope(ctx context.Context, envelope *roomeventsv1.EventEnvelope, options RoomNoticeWorkerOptions) (bool, error) {
|
|
if envelope == nil || envelope.GetEventType() != eventRoomUserKicked {
|
|
return false, nil
|
|
}
|
|
options = normalizeRoomNoticeWorkerOptions(options, s.cfg.NodeID)
|
|
if s.repository == nil {
|
|
return true, fmt.Errorf("notice repository is not configured")
|
|
}
|
|
if s.publisher == nil {
|
|
return true, fmt.Errorf("notice publisher is not configured")
|
|
}
|
|
event, claimed, err := s.repository.ClaimRoomKickEnvelope(ctx, options.WorkerID, envelope, options.LockTTL)
|
|
if err != nil || !claimed {
|
|
return true, err
|
|
}
|
|
return true, s.publishRoomKickEvent(ctx, event, options)
|
|
}
|
|
|
|
func (s *Service) publishRoomKickEvent(ctx context.Context, event RoomKickEvent, options RoomNoticeWorkerOptions) error {
|
|
payload, err := roomKickNoticePayload(event)
|
|
if err != nil {
|
|
return s.markFailed(ctx, event, options, err)
|
|
}
|
|
publishCtx, cancel := context.WithTimeout(ctx, options.PublishTimeout)
|
|
defer cancel()
|
|
err = s.publisher.PublishUserCustomMessage(publishCtx, tencentim.CustomUserMessage{
|
|
ToAccount: tencentim.FormatUserID(event.TargetUserID),
|
|
EventID: event.EventID,
|
|
Desc: "room_user_kicked",
|
|
Ext: "room_notice",
|
|
PayloadJSON: payload,
|
|
})
|
|
nowMs := time.Now().UnixMilli()
|
|
if err == nil {
|
|
if markErr := s.repository.MarkRoomKickDelivered(ctx, event, payload, nowMs); markErr != nil {
|
|
return markErr
|
|
}
|
|
logx.Info(ctx, "notice_room_kick_delivered",
|
|
slog.String("event_id", event.EventID),
|
|
slog.String("app_code", event.AppCode),
|
|
slog.String("room_id", event.RoomID),
|
|
slog.Int64("target_user_id", event.TargetUserID),
|
|
)
|
|
return nil
|
|
}
|
|
return s.markFailed(ctx, event, options, err)
|
|
}
|
|
|
|
func (s *Service) markFailed(ctx context.Context, event RoomKickEvent, options RoomNoticeWorkerOptions, cause error) error {
|
|
nowMs := time.Now().UnixMilli()
|
|
nextRetryCount := event.RetryCount + 1
|
|
if nextRetryCount >= options.MaxRetryCount {
|
|
if err := s.repository.MarkRoomKickFailed(ctx, event, nextRetryCount, cause.Error(), nowMs); err != nil {
|
|
return err
|
|
}
|
|
logx.Error(ctx, "notice_room_kick_dead_letter", cause,
|
|
slog.String("event_id", event.EventID),
|
|
slog.String("app_code", event.AppCode),
|
|
slog.String("room_id", event.RoomID),
|
|
slog.Int64("target_user_id", event.TargetUserID),
|
|
slog.Int("retry_count", nextRetryCount),
|
|
)
|
|
return nil
|
|
}
|
|
nextRetryAtMS := nowMs + roomNoticeBackoff(nextRetryCount, options).Milliseconds()
|
|
if err := s.repository.MarkRoomKickRetryable(ctx, event, nextRetryCount, nextRetryAtMS, cause.Error(), nowMs); err != nil {
|
|
return err
|
|
}
|
|
logx.Error(ctx, "notice_room_kick_retryable", cause,
|
|
slog.String("event_id", event.EventID),
|
|
slog.String("app_code", event.AppCode),
|
|
slog.String("room_id", event.RoomID),
|
|
slog.Int64("target_user_id", event.TargetUserID),
|
|
slog.Int("retry_count", nextRetryCount),
|
|
slog.Int64("next_retry_at_ms", nextRetryAtMS),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
func roomKickNoticePayload(event RoomKickEvent) (json.RawMessage, error) {
|
|
payload := map[string]any{
|
|
"event_id": event.EventID,
|
|
"event_type": "room_user_kicked",
|
|
"source_event_type": event.EventType,
|
|
"app_code": event.AppCode,
|
|
"room_id": event.RoomID,
|
|
"room_version": event.RoomVersion,
|
|
"actor_user_id": event.ActorUserID,
|
|
"target_user_id": event.TargetUserID,
|
|
"occurred_at_ms": event.OccurredAtMS,
|
|
"source_created_at_ms": event.CreatedAtMS,
|
|
}
|
|
return json.Marshal(payload)
|
|
}
|