Files
smartmate/backend/services/tokenstore/sv/outbox.go
2026-05-06 00:30:08 +08:00

158 lines
5.3 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 sv
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"strconv"
"strings"
tokencontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/tokenstore"
sharedevents "github.com/LoveLosita/smartflow/backend/shared/events"
kafkabus "github.com/LoveLosita/smartflow/backend/shared/infra/kafka"
outboxinfra "github.com/LoveLosita/smartflow/backend/shared/infra/outbox"
)
// OutboxBus 是 token-store 注册论坛奖励 handler 需要的最小总线接口。
type OutboxBus interface {
RegisterEventHandler(eventType string, handler outboxinfra.MessageHandler) error
}
// RegisterForumRewardRoutes 只登记 token-store 负责消费的论坛奖励事件归属。
//
// 职责边界:
// 1. 只负责把事件类型路由到 token-store 的服务级 outbox
// 2. 不注册 handler也不启动 consumer
// 3. 供发布方和消费方在不同进程内共享同一份事件归属映射。
func RegisterForumRewardRoutes() error {
if err := outboxinfra.RegisterEventService(sharedevents.ForumPostLikedEventType, outboxinfra.ServiceTokenStore); err != nil {
return err
}
return outboxinfra.RegisterEventService(sharedevents.ForumPostImportedEventType, outboxinfra.ServiceTokenStore)
}
// RegisterForumRewardHandlers 注册 token-store 对论坛奖励事件的消费处理器。
//
// 职责边界:
// 1. 只消费 forum.post.liked / forum.post.imported 两类事件;
// 2. 论坛奖励账本写入由 token-store Service 负责handler 不自行计算金额;
// 3. grant 写入成功后再标记 consumed若标记失败可依赖 event_id 幂等安全重试。
func RegisterForumRewardHandlers(bus OutboxBus, outboxRepo *outboxinfra.Repository, svc *Service) error {
if bus == nil {
return errors.New("event bus is nil")
}
if outboxRepo == nil {
return errors.New("outbox repository is nil")
}
if svc == nil {
return errors.New("tokenstore service is nil")
}
if err := RegisterForumRewardRoutes(); err != nil {
return err
}
if err := registerForumRewardHandler(bus, outboxRepo, svc, sharedevents.ForumPostLikedEventType, sharedevents.ForumRewardSourceLike); err != nil {
return err
}
return registerForumRewardHandler(bus, outboxRepo, svc, sharedevents.ForumPostImportedEventType, sharedevents.ForumRewardSourceImport)
}
func registerForumRewardHandler(
bus OutboxBus,
outboxRepo *outboxinfra.Repository,
svc *Service,
eventType string,
source string,
) error {
route, ok := outboxinfra.ResolveEventRoute(eventType)
if !ok {
return fmt.Errorf("forum reward outbox route is missing: eventType=%s", eventType)
}
eventOutboxRepo := outboxRepo.WithRoute(route)
handler := func(ctx context.Context, envelope kafkabus.Envelope) error {
if !isAllowedForumRewardEventVersion(envelope.EventVersion) {
if err := eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, fmt.Sprintf("论坛奖励事件版本不受支持: %s", envelope.EventVersion)); err != nil {
return err
}
return nil
}
var payload sharedevents.ForumPostRewardPayload
if err := json.Unmarshal(envelope.Payload, &payload); err != nil {
if markErr := eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "解析论坛奖励载荷失败: "+err.Error()); markErr != nil {
return markErr
}
return nil
}
if err := payload.Validate(); err != nil {
if markErr := eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "论坛奖励载荷非法: "+err.Error()); markErr != nil {
return markErr
}
return nil
}
if payload.EventType() != eventType {
if markErr := eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, fmt.Sprintf("论坛奖励事件类型不匹配: envelope=%s payload=%s", eventType, payload.EventType())); markErr != nil {
return markErr
}
return nil
}
eventID := strings.TrimSpace(envelope.EventID)
if eventID == "" {
eventID = strings.TrimSpace(payload.EventID)
}
if eventID == "" {
if markErr := eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "论坛奖励 event_id 为空"); markErr != nil {
return markErr
}
return nil
}
grant, err := svc.RecordForumRewardGrant(ctx, tokencontracts.RecordForumRewardGrantRequest{
EventID: eventID,
ReceiverUserID: payload.RewardReceiverUserID,
Source: forumRewardSource(payload, source),
SourceRefID: forumRewardSourceRefID(payload, source),
})
if err != nil {
return err
}
if err := eventOutboxRepo.MarkConsumed(ctx, envelope.OutboxID); err != nil {
return err
}
log.Printf(
"forum reward event consumed by tokenstore: event_type=%s event_id=%s grant_id=%d outbox_id=%d",
eventType,
eventID,
grant.GrantID,
envelope.OutboxID,
)
return nil
}
return bus.RegisterEventHandler(eventType, handler)
}
func isAllowedForumRewardEventVersion(version string) bool {
version = strings.TrimSpace(version)
return version == "" || version == sharedevents.ForumRewardEventVersion
}
func forumRewardSource(payload sharedevents.ForumPostRewardPayload, fallback string) string {
source := strings.TrimSpace(payload.Source)
if source != "" {
return source
}
return fallback
}
func forumRewardSourceRefID(payload sharedevents.ForumPostRewardPayload, source string) string {
if source == sharedevents.ForumRewardSourceImport && payload.ImportID > 0 {
return strconv.FormatUint(payload.ImportID, 10)
}
return strconv.FormatUint(payload.PostID, 10)
}