2026-06-25 19:57:16 +08:00

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
}