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

154 lines
5.5 KiB
Go

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
workerCtx := appcode.WithContext(ctx, appcode.Default)
records, err := a.actionConfirm.ClaimPendingOutbox(workerCtx, 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()
}