2026-04-27 02:29:42 +08:00

142 lines
5.1 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 service
import (
"context"
"fmt"
"time"
roomeventsv1 "hyapp/api/proto/events/room/v1"
roomv1 "hyapp/api/proto/room/v1"
walletv1 "hyapp/api/proto/wallet/v1"
"hyapp/pkg/tencentim"
"hyapp/pkg/xerr"
"hyapp/services/room-service/internal/room/command"
"hyapp/services/room-service/internal/room/outbox"
"hyapp/services/room-service/internal/room/rank"
"hyapp/services/room-service/internal/room/state"
)
// SendGift 先同步调用 wallet再在 Room Cell 内完成热度、榜单和房间事件落地。
func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*roomv1.SendGiftResponse, error) {
// SendGift 的账务不在 room-service 内结算,但房间表现、热度和榜单由 Room Cell 同步更新。
cmd := command.SendGift{
Base: baseFromMeta(req.GetMeta()),
TargetUserID: req.GetTargetUserId(),
GiftID: req.GetGiftId(),
GiftCount: req.GetGiftCount(),
GiftUnitValue: req.GetGiftUnitValue(),
}
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, localRank *rank.LocalRank) (mutationResult, []outbox.Record, error) {
if _, exists := current.OnlineUsers[cmd.ActorUserID()]; !exists {
// 送礼者必须在房间业务 presence 内。
return mutationResult{}, nil, xerr.New(xerr.NotFound, "sender not in room")
}
if _, exists := current.OnlineUsers[cmd.TargetUserID]; !exists {
// v1 要求礼物目标在房间内,避免给不存在的房间表现目标计榜。
return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not in room")
}
if cmd.GiftCount <= 0 || cmd.GiftUnitValue <= 0 {
// 非正数礼物会破坏账务和榜单累计语义。
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "gift_count and gift_unit_value must be positive")
}
// 钱包扣费在房间状态变更前完成;失败时不写 command log、不进入 Room Cell 提交态。
billing, err := s.wallet.DebitGift(ctx, &walletv1.DebitGiftRequest{
CommandId: cmd.ID(),
RoomId: cmd.RoomID(),
SenderUserId: cmd.ActorUserID(),
TargetUserId: cmd.TargetUserID,
GiftId: cmd.GiftID,
GiftCount: cmd.GiftCount,
GiftUnitValue: cmd.GiftUnitValue,
})
if err != nil {
return mutationResult{}, nil, err
}
// 扣费成功后Room Cell 同步更新热度和本地礼物榜。
current.Heat += billing.GetTotalGiftValue()
current.GiftRank = localRank.ApplyGift(cmd.ActorUserID(), billing.GetTotalGiftValue(), now)
current.Version++
// 送礼事件、热度事件和榜单事件使用同一个房间版本,方便消费者关联同一次命令。
giftEvent, err := outbox.Build(current.RoomID, "RoomGiftSent", current.Version, now, &roomeventsv1.RoomGiftSent{
SenderUserId: cmd.ActorUserID(),
TargetUserId: cmd.TargetUserID,
GiftId: cmd.GiftID,
GiftCount: cmd.GiftCount,
GiftValue: billing.GetTotalGiftValue(),
BillingReceiptId: billing.GetBillingReceiptId(),
})
if err != nil {
return mutationResult{}, nil, err
}
heatEvent, err := outbox.Build(current.RoomID, "RoomHeatChanged", current.Version, now, &roomeventsv1.RoomHeatChanged{
Delta: billing.GetTotalGiftValue(),
CurrentHeat: current.Heat,
})
if err != nil {
return mutationResult{}, nil, err
}
rankItem := findRankItem(current.GiftRank, cmd.ActorUserID())
rankEvent, err := outbox.Build(current.RoomID, "RoomRankChanged", current.Version, now, &roomeventsv1.RoomRankChanged{
UserId: rankItem.UserID,
Score: rankItem.Score,
GiftValue: rankItem.GiftValue,
})
if err != nil {
return mutationResult{}, nil, err
}
return mutationResult{
snapshot: current.ToProto(),
billingReceiptID: billing.GetBillingReceiptId(),
roomHeat: current.Heat,
giftRank: cloneProtoRank(current.GiftRank),
syncEvent: &tencentim.RoomEvent{
// 同步广播只选 GiftSent 主事件作为客户端房间系统消息Heat/Rank 通过字段携带。
EventID: giftEvent.EventID,
RoomID: current.RoomID,
EventType: "room_gift_sent",
ActorUserID: cmd.ActorUserID(),
TargetUserID: cmd.TargetUserID,
GiftValue: billing.GetTotalGiftValue(),
RoomHeat: current.Heat,
RoomVersion: current.Version,
Attributes: map[string]string{
"gift_id": cmd.GiftID,
"gift_count": fmt.Sprintf("%d", cmd.GiftCount),
"billing_receipt_id": billing.GetBillingReceiptId(),
},
},
}, []outbox.Record{giftEvent, heatEvent, rankEvent}, nil
})
if err != nil {
return nil, err
}
return &roomv1.SendGiftResponse{
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
BillingReceiptId: result.billingReceiptID,
RoomHeat: result.roomHeat,
GiftRank: result.giftRank,
Room: result.snapshot,
}, nil
}
func findRankItem(items []state.RankItem, userID int64) state.RankItem {
// 送礼后通常能找到发送方榜单项;找不到时返回 user_id 占位避免空事件。
for _, item := range items {
if item.UserID == userID {
return item
}
}
return state.RankItem{UserID: userID}
}