169 lines
6.2 KiB
Go
169 lines
6.2 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
|
||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||
roomv1 "hyapp.local/api/proto/room/v1"
|
||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||
"hyapp/pkg/appcode"
|
||
"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) {
|
||
ctx = contextFromMeta(ctx, req.GetMeta())
|
||
// SendGift 的账务不在 room-service 内结算,但房间表现、热度和榜单由 Room Cell 同步更新。
|
||
cmd := command.SendGift{
|
||
Base: baseFromMeta(req.GetMeta()),
|
||
TargetUserID: req.GetTargetUserId(),
|
||
GiftID: req.GetGiftId(),
|
||
GiftCount: req.GetGiftCount(),
|
||
}
|
||
|
||
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.GiftID == "" || cmd.GiftCount <= 0 {
|
||
// 礼物 ID 和数量是钱包查服务端价格的最小输入,客户端不再提交礼物单价。
|
||
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "gift_id and gift_count must be valid")
|
||
}
|
||
roomMeta, exists, err := s.repository.GetRoomMeta(ctx, cmd.RoomID())
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
if !exists {
|
||
return mutationResult{}, nil, xerr.New(xerr.NotFound, "room not found")
|
||
}
|
||
|
||
// 钱包扣费在房间状态变更前完成;失败时不写 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,
|
||
AppCode: appcode.FromContext(ctx),
|
||
RegionId: roomMeta.VisibleRegionID,
|
||
})
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
if billing == nil {
|
||
return mutationResult{}, nil, xerr.New(xerr.Unavailable, "wallet debit response is empty")
|
||
}
|
||
heatValue := billing.GetHeatValue()
|
||
settledCommand := cmd
|
||
settledCommand.BillingReceiptID = billing.GetBillingReceiptId()
|
||
settledCommand.CoinSpent = billing.GetCoinSpent()
|
||
settledCommand.GiftPointAdded = billing.GetGiftPointAdded()
|
||
settledCommand.HeatValue = heatValue
|
||
settledCommand.PriceVersion = billing.GetPriceVersion()
|
||
commandPayload, err := command.Serialize(settledCommand)
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
|
||
// 扣费成功后,Room Cell 同步更新热度和本地礼物榜。
|
||
current.Heat += heatValue
|
||
current.GiftRank = localRank.ApplyGift(cmd.ActorUserID(), heatValue, 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: heatValue,
|
||
BillingReceiptId: billing.GetBillingReceiptId(),
|
||
})
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
|
||
heatEvent, err := outbox.Build(current.RoomID, "RoomHeatChanged", current.Version, now, &roomeventsv1.RoomHeatChanged{
|
||
Delta: heatValue,
|
||
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),
|
||
commandPayload: commandPayload,
|
||
syncEvent: &tencentim.RoomEvent{
|
||
// 同步广播只选 GiftSent 主事件作为客户端房间系统消息,Heat/Rank 通过字段携带。
|
||
EventID: giftEvent.EventID,
|
||
RoomID: current.RoomID,
|
||
EventType: "room_gift_sent",
|
||
ActorUserID: cmd.ActorUserID(),
|
||
TargetUserID: cmd.TargetUserID,
|
||
GiftValue: heatValue,
|
||
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(),
|
||
"coin_spent": fmt.Sprintf("%d", settledCommand.CoinSpent),
|
||
"gift_point_added": fmt.Sprintf("%d", settledCommand.GiftPointAdded),
|
||
"price_version": settledCommand.PriceVersion,
|
||
},
|
||
},
|
||
}, []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}
|
||
}
|