136 lines
4.9 KiB
Go
136 lines
4.9 KiB
Go
package events
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
|
||
kafkabus "github.com/LoveLosita/smartflow/backend/infra/kafka"
|
||
outboxinfra "github.com/LoveLosita/smartflow/backend/infra/outbox"
|
||
tokencontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/tokenstore"
|
||
sharedevents "github.com/LoveLosita/smartflow/backend/shared/events"
|
||
)
|
||
|
||
// ForumRewardGrantRecorder 描述论坛奖励事件消费后写入 token-store 账本所需的最小能力。
|
||
//
|
||
// 职责边界:
|
||
// 1. 只暴露论坛奖励入账能力,不暴露商品、订单和用户可见查询接口;
|
||
// 2. 由 token-store 自己解析奖励额度和幂等规则,handler 不计算 Token 数量;
|
||
// 3. 接口用于隔离 service/events 与 token-store 具体 RPC client 实现。
|
||
type ForumRewardGrantRecorder interface {
|
||
RecordForumRewardGrant(ctx context.Context, req tokencontracts.RecordForumRewardGrantRequest) (*tokencontracts.TokenGrantView, error)
|
||
}
|
||
|
||
// RegisterForumPostLikedRewardHandler 注册计划被点赞奖励消费者。
|
||
func RegisterForumPostLikedRewardHandler(
|
||
bus OutboxBus,
|
||
outboxRepo *outboxinfra.Repository,
|
||
recorder ForumRewardGrantRecorder,
|
||
) error {
|
||
return registerForumRewardHandler(bus, outboxRepo, recorder, sharedevents.ForumPostLikedEventType, sharedevents.ForumRewardSourceLike)
|
||
}
|
||
|
||
// RegisterForumPostImportedRewardHandler 注册计划被导入奖励消费者。
|
||
func RegisterForumPostImportedRewardHandler(
|
||
bus OutboxBus,
|
||
outboxRepo *outboxinfra.Repository,
|
||
recorder ForumRewardGrantRecorder,
|
||
) error {
|
||
return registerForumRewardHandler(bus, outboxRepo, recorder, sharedevents.ForumPostImportedEventType, sharedevents.ForumRewardSourceImport)
|
||
}
|
||
|
||
// registerForumRewardHandler 收敛论坛奖励事件的通用解析、校验和入账流程。
|
||
//
|
||
// 步骤说明:
|
||
// 1. 先校验 outbox 与 token-store 依赖,避免启动期注册半截 handler;
|
||
// 2. 消费时先检查版本和 payload,明显不可修复的坏消息直接标记 dead;
|
||
// 3. 再调用 token-store 内部 RPC 幂等写 token_grants,RPC 临时失败返回 error 交给 outbox 重试;
|
||
// 4. 入账成功后标记 consumed,确保重复消费不会重复发放。
|
||
func registerForumRewardHandler(
|
||
bus OutboxBus,
|
||
outboxRepo *outboxinfra.Repository,
|
||
recorder ForumRewardGrantRecorder,
|
||
eventType string,
|
||
source string,
|
||
) error {
|
||
if bus == nil {
|
||
return errors.New("event bus is nil")
|
||
}
|
||
if outboxRepo == nil {
|
||
return errors.New("outbox repository is nil")
|
||
}
|
||
if recorder == nil {
|
||
return errors.New("forum reward grant recorder is nil")
|
||
}
|
||
eventOutboxRepo, err := scopedOutboxRepoForEvent(outboxRepo, eventType)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
handler := func(ctx context.Context, envelope kafkabus.Envelope) error {
|
||
if !isAllowedForumRewardEventVersion(envelope.EventVersion) {
|
||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, fmt.Sprintf("论坛奖励事件版本不受支持: %s", envelope.EventVersion))
|
||
return nil
|
||
}
|
||
|
||
var payload sharedevents.ForumPostRewardPayload
|
||
if unmarshalErr := json.Unmarshal(envelope.Payload, &payload); unmarshalErr != nil {
|
||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "解析论坛奖励载荷失败: "+unmarshalErr.Error())
|
||
return nil
|
||
}
|
||
if validateErr := payload.Validate(); validateErr != nil {
|
||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "论坛奖励载荷非法: "+validateErr.Error())
|
||
return nil
|
||
}
|
||
if payload.EventType() != eventType {
|
||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, fmt.Sprintf("论坛奖励事件类型不匹配: envelope=%s payload=%s", eventType, payload.EventType()))
|
||
return nil
|
||
}
|
||
|
||
eventID := strings.TrimSpace(envelope.EventID)
|
||
if eventID == "" {
|
||
eventID = strings.TrimSpace(payload.EventID)
|
||
}
|
||
if eventID == "" {
|
||
_ = eventOutboxRepo.MarkDead(ctx, envelope.OutboxID, "论坛奖励 event_id 为空")
|
||
return nil
|
||
}
|
||
|
||
_, err := recorder.RecordForumRewardGrant(ctx, tokencontracts.RecordForumRewardGrantRequest{
|
||
EventID: eventID,
|
||
ReceiverUserID: payload.RewardReceiverUserID,
|
||
Source: forumRewardSource(payload, source),
|
||
SourceRefID: forumRewardSourceRefID(payload, source),
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return eventOutboxRepo.MarkConsumed(ctx, envelope.OutboxID)
|
||
}
|
||
|
||
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)
|
||
}
|