155 lines
5.6 KiB
Go
155 lines
5.6 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
|
||
// 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()
|
||
}
|