161 lines
5.3 KiB
Go
161 lines
5.3 KiB
Go
// 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
|
|
}
|