// Package confirmnotice consumes activity-service confirm-message facts and delivers C2C IM. package confirmnotice import ( "context" "encoding/json" "fmt" "log/slog" "strconv" "time" "hyapp/pkg/appcode" "hyapp/pkg/logx" "hyapp/pkg/messagemq" "hyapp/pkg/tencentim" ) const ( sourceMessageActionOutbox = "message_action_outbox" channelTencentIMC2C = "tencent_im_c2c" noticeTypeIMConfirm = "im_confirm" ) // Repository owns notice_delivery_events rows for confirm IM delivery. type Repository interface { ClaimConfirmDelivery(ctx context.Context, workerID string, delivery Delivery, lockTTL time.Duration) (Delivery, bool, error) MarkConfirmDelivered(ctx context.Context, delivery Delivery, deliveredPayload []byte, nowMs int64) error MarkConfirmRetryable(ctx context.Context, delivery Delivery, retryCount int, nextRetryAtMS int64, lastErr string, nowMs int64) error MarkConfirmFailed(ctx context.Context, delivery Delivery, retryCount int, lastErr string, nowMs int64) error } // Publisher is the Tencent IM C2C boundary. type Publisher interface { PublishUserCustomMessage(ctx context.Context, message tencentim.CustomUserMessage) error } // Config carries process-level metadata for worker IDs. type Config struct { NodeID string } // Delivery is one concrete C2C IM side effect derived from message_action_outbox. type Delivery struct { AppCode string EventID string EventType string ConfirmID string Channel string NoticeType string SenderUserID int64 TargetUserID int64 PayloadJSON []byte RetryCount int CreatedAtMS int64 } // Service maps confirm-message outbox facts to Tencent IM custom C2C messages. type Service struct { cfg Config repository Repository publisher Publisher } func New(cfg Config, repository Repository, publisher Publisher) *Service { return &Service{cfg: cfg, repository: repository, publisher: publisher} } // ProcessActionOutboxMessage handles one confirm-message MQ fact and acks unrelated messages. func (s *Service) ProcessActionOutboxMessage(ctx context.Context, message messagemq.ActionOutboxMessage, options WorkerOptions) (bool, error) { if message.EventType != "ConfirmMessageCreated" { return false, nil } options = normalizeWorkerOptions(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") } delivery, err := deliveryFromMessage(message) if err != nil { return true, err } claimed, ok, err := s.repository.ClaimConfirmDelivery(ctx, options.WorkerID, delivery, options.LockTTL) if err != nil || !ok { return true, err } return true, s.publishDelivery(ctx, claimed, options) } func (s *Service) publishDelivery(ctx context.Context, delivery Delivery, options WorkerOptions) error { publishCtx, cancel := context.WithTimeout(ctx, options.PublishTimeout) defer cancel() err := s.publisher.PublishUserCustomMessage(publishCtx, tencentim.CustomUserMessage{ ToAccount: tencentim.FormatUserID(delivery.TargetUserID), FromAccount: tencentim.FormatUserID(delivery.SenderUserID), EventID: delivery.EventID, Desc: delivery.NoticeType, Ext: "im_confirm", PayloadJSON: delivery.PayloadJSON, }) nowMs := time.Now().UnixMilli() if err == nil { if markErr := s.repository.MarkConfirmDelivered(ctx, delivery, delivery.PayloadJSON, nowMs); markErr != nil { return markErr } logx.Info(ctx, "notice_confirm_delivered", slog.String("event_id", delivery.EventID), slog.String("app_code", delivery.AppCode), slog.String("confirm_id", delivery.ConfirmID), slog.Int64("target_user_id", delivery.TargetUserID), ) return nil } nextRetryCount := delivery.RetryCount + 1 if nextRetryCount >= options.MaxRetryCount { if markErr := s.repository.MarkConfirmFailed(ctx, delivery, nextRetryCount, err.Error(), nowMs); markErr != nil { return markErr } logx.Error(ctx, "notice_confirm_dead_letter", err, slog.String("event_id", delivery.EventID), slog.String("confirm_id", delivery.ConfirmID)) return nil } nextRetryAtMS := nowMs + backoff(nextRetryCount, options).Milliseconds() if markErr := s.repository.MarkConfirmRetryable(ctx, delivery, nextRetryCount, nextRetryAtMS, err.Error(), nowMs); markErr != nil { return markErr } return err } type confirmPayload struct { SenderUserID string `json:"sender_user_id"` ReceiverUserID string `json:"receiver_user_id"` } func deliveryFromMessage(message messagemq.ActionOutboxMessage) (Delivery, error) { var payload confirmPayload if err := json.Unmarshal([]byte(message.PayloadJSON), &payload); err != nil { return Delivery{}, err } senderID, err := strconv.ParseInt(payload.SenderUserID, 10, 64) if err != nil || senderID <= 0 { return Delivery{}, fmt.Errorf("sender_user_id is invalid") } receiverID, err := strconv.ParseInt(payload.ReceiverUserID, 10, 64) if err != nil || receiverID <= 0 { return Delivery{}, fmt.Errorf("receiver_user_id is invalid") } return Delivery{ AppCode: appcode.Normalize(message.AppCode), EventID: message.EventID, EventType: message.EventType, ConfirmID: message.ConfirmID, Channel: channelTencentIMC2C, NoticeType: noticeTypeIMConfirm, SenderUserID: senderID, TargetUserID: receiverID, PayloadJSON: []byte(message.PayloadJSON), CreatedAtMS: message.OccurredAtMS, }, nil }