2026-07-23 16:47:40 +08:00

155 lines
5.6 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 app
import (
"context"
"encoding/json"
"log/slog"
"time"
"hyapp/pkg/appcode"
"hyapp/pkg/logx"
"hyapp/pkg/messagemq"
"hyapp/pkg/rocketmqx"
"hyapp/pkg/usermq"
"hyapp/services/activity-service/internal/config"
messagedomain "hyapp/services/activity-service/internal/domain/message"
)
const userOutboxEventRoleInvitationCreated = "RoleInvitationCreated"
func newMessageActionUserOutboxConsumer(cfg config.Config, services *serviceBundle) (*rocketmqx.Consumer, error) {
consumer, err := rocketmqx.NewConsumer(userOutboxMessageActionConsumerConfig(cfg.RocketMQ))
if err != nil {
return nil, err
}
if err := consumer.Subscribe(cfg.RocketMQ.UserOutbox.Topic, usermq.TagUserOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
event, appCode, ok, err := roleInvitationConfirmEventFromUserMessage(message.Body)
if err != nil || !ok {
return err
}
_, _, err = services.actionConfirm.ConsumeRoleInvitationCreated(appcode.WithContext(ctx, appCode), event)
return err
}); err != nil {
_ = consumer.Shutdown()
return nil, err
}
return consumer, nil
}
type roleInvitationCreatedPayload struct {
InvitationID int64 `json:"invitation_id"`
InvitationType string `json:"invitation_type"`
InviterUserID int64 `json:"inviter_user_id"`
TargetUserID int64 `json:"target_user_id"`
AgencyName string `json:"agency_name"`
Inviter messagedomain.RoleInvitationUserSnapshot `json:"inviter"`
Target messagedomain.RoleInvitationUserSnapshot `json:"target"`
CreatedAtMS int64 `json:"created_at_ms"`
ParentBDUserID int64 `json:"parent_bd_user_id"`
ParentLeaderUserID int64 `json:"parent_leader_user_id"`
}
func roleInvitationConfirmEventFromUserMessage(body []byte) (messagedomain.RoleInvitationConfirmEvent, string, bool, error) {
message, err := usermq.DecodeUserOutboxMessage(body)
if err != nil {
return messagedomain.RoleInvitationConfirmEvent{}, "", false, err
}
if message.EventType != userOutboxEventRoleInvitationCreated {
return messagedomain.RoleInvitationConfirmEvent{}, message.AppCode, false, nil
}
var payload roleInvitationCreatedPayload
if err := json.Unmarshal([]byte(message.PayloadJSON), &payload); err != nil {
return messagedomain.RoleInvitationConfirmEvent{}, message.AppCode, true, err
}
return messagedomain.RoleInvitationConfirmEvent{
SourceEventID: message.EventID,
InvitationID: payload.InvitationID,
InvitationType: payload.InvitationType,
InviterUserID: payload.InviterUserID,
TargetUserID: payload.TargetUserID,
AgencyName: payload.AgencyName,
Inviter: payload.Inviter,
Target: payload.Target,
CreatedAtMS: firstPositiveMS(payload.CreatedAtMS, message.OccurredAtMS),
RawPayloadJSON: message.PayloadJSON,
ParentBDUserID: payload.ParentBDUserID,
ParentLeaderUserID: payload.ParentLeaderUserID,
}, message.AppCode, true, nil
}
func (a *App) runMessageActionOutboxWorker(ctx context.Context) {
options := a.messageActionWorkerOptions
ticker := time.NewTicker(options.OutboxPollInterval)
defer ticker.Stop()
for {
processed, err := a.processMessageActionOutboxBatch(ctx)
if err != nil && ctx.Err() == nil {
logx.Error(ctx, "message_action_outbox_publish_failed", err, slog.String("node_id", a.workerNodeID))
}
if processed >= options.OutboxBatchSize {
continue
}
select {
case <-ctx.Done():
return
case <-ticker.C:
}
}
}
func (a *App) processMessageActionOutboxBatch(ctx context.Context) (int, error) {
options := a.messageActionWorkerOptions
// ActionConfirmService 会按配置的 App 分区公平 claim这里不能再固定默认 Lalu context
// 否则 Fami/Huwaa 已落库的确认消息永远不会发布到 notice-service。
records, err := a.actionConfirm.ClaimPendingOutbox(ctx, options.OutboxBatchSize, options.OutboxLockTTL)
if err != nil {
return 0, err
}
for _, record := range records {
publishCtx, cancel := context.WithTimeout(context.Background(), options.PublishTimeout)
err := a.publishMessageActionOutboxRecord(publishCtx, record)
cancel()
recordCtx := appcode.WithContext(context.Background(), record.AppCode)
if err != nil {
nextRetryAtMS := time.Now().UTC().Add(options.OutboxPollInterval).UnixMilli()
if markErr := a.actionConfirm.MarkOutboxRetryable(recordCtx, record.EventID, err, nextRetryAtMS); markErr != nil {
return len(records), markErr
}
return len(records), err
}
if err := a.actionConfirm.MarkOutboxDelivered(recordCtx, record.EventID); err != nil {
return len(records), err
}
}
return len(records), nil
}
func (a *App) publishMessageActionOutboxRecord(ctx context.Context, record messagedomain.ActionConfirmOutbox) error {
body, err := messagemq.EncodeActionOutboxMessage(messagemq.ActionOutboxMessage{
AppCode: record.AppCode,
EventID: record.EventID,
EventType: record.EventType,
ConfirmID: record.ConfirmID,
PayloadJSON: record.PayloadJSON,
OccurredAtMS: record.CreatedAtMS,
})
if err != nil {
return err
}
return a.messageActionProducer.SendSync(ctx, rocketmqx.Message{
Topic: a.messageActionOutboxTopic,
Tag: messagemq.TagMessageActionOutboxEvent,
Keys: []string{record.EventID, record.ConfirmID},
Body: body,
})
}
func firstPositiveMS(values ...int64) int64 {
for _, value := range values {
if value > 0 {
return value
}
}
return time.Now().UTC().UnixMilli()
}