package service import ( "context" "strings" "time" roomv1 "hyapp.local/api/proto/room/v1" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/appcode" "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" ) // ApplyRoomRPSGift 把 room-rps 最终由败者支付的一份礼物应用到火箭。 // 钱包已由 game-service 结算,因此本入口只接受服务端锁定的 heat_value,并用 command_id 保证重试不重复充能。 func (s *Service) ApplyRoomRPSGift(ctx context.Context, req *roomv1.ApplyRoomRPSGiftRequest) (*roomv1.ApplyRoomRPSGiftResponse, error) { ctx = contextFromMeta(ctx, req.GetMeta()) if req.GetSenderUserId() <= 0 || req.GetTargetUserId() <= 0 || strings.TrimSpace(req.GetGiftId()) == "" || req.GetHeatValue() <= 0 { return nil, xerr.New(xerr.InvalidArgument, "room rps settled gift is incomplete") } cmd := command.ApplyRoomRPSGift{ Base: baseFromMeta(req.GetMeta()), TargetUserID: req.GetTargetUserId(), GiftID: strings.TrimSpace(req.GetGiftId()), HeatValue: req.GetHeatValue(), } cmd.Base.Room = strings.TrimSpace(req.GetRoomId()) cmd.Base.ActorID = req.GetSenderUserId() result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) { 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") } cfg, err := s.roomRocketConfig(ctx) if err != nil { return mutationResult{}, nil, err } // 复用普通送礼的唯一火箭规则实现,但不进入钱包、房间热度、礼物榜或 RoomGiftSent 链路。 giftCmd := command.SendGift{Base: cmd.Base, TargetUserID: cmd.TargetUserID, TargetUserIDs: []int64{cmd.TargetUserID}, GiftID: cmd.GiftID, GiftCount: 1, HeatValue: cmd.HeatValue} settledGift := giftCmd billing := &walletv1.DebitGiftResponse{HeatValue: cmd.HeatValue} rocketApply, err := s.applyRoomRocketGift(now, current, cfg, giftCmd, &settledGift, billing, roomMeta) if err != nil { return mutationResult{}, nil, err } copyRoomRPSRocketSnapshot(&cmd, settledGift) payload, err := command.Serialize(cmd) if err != nil { return mutationResult{}, nil, err } current.Version++ records := make([]outbox.Record, 0, 1) var progress *roomRocketProgressPending if rocketApply.progressEvent != nil { progressRecord, err := outbox.Build(current.RoomID, "RoomRocketFuelChanged", current.Version, now, rocketApply.progressEvent) if err != nil { return mutationResult{}, nil, err } progressRecord.AppCode = appcode.FromContext(ctx) if progressRecord.Envelope != nil { progressRecord.Envelope.AppCode = progressRecord.AppCode } progress = &roomRocketProgressPending{record: progressRecord, rocketID: rocketApply.progressEvent.GetRocketId(), level: rocketApply.progressEvent.GetLevel(), currentFuel: rocketApply.progressEvent.GetCurrentFuel(), roomVersion: current.Version} } if rocketApply.ignited != nil { ignitedRecord, err := outbox.Build(current.RoomID, "RoomRocketIgnited", current.Version, now, rocketApply.ignited) if err != nil { return mutationResult{}, nil, err } records = append(records, ignitedRecord) } return mutationResult{snapshot: current.ToProto(), commandPayload: payload, roomRocketProgress: progress}, records, nil }) if err != nil { return nil, err } return &roomv1.ApplyRoomRPSGiftResponse{Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()), Rocket: result.snapshot.GetRocket()}, nil } func copyRoomRPSRocketSnapshot(target *command.ApplyRoomRPSGift, source command.SendGift) { target.RocketTouched = source.RocketTouched target.RocketAddedFuel = source.RocketAddedFuel target.RocketEffectiveFuel = source.RocketEffectiveFuel target.RocketOverflowFuel = source.RocketOverflowFuel target.RocketLevel = source.RocketLevel target.RocketFuel = source.RocketFuel target.RocketStatus = source.RocketStatus target.RocketFuelThreshold = source.RocketFuelThreshold target.RocketIgnitedAtMS = source.RocketIgnitedAtMS target.RocketLaunchAtMS = source.RocketLaunchAtMS target.RocketResetAtMS = source.RocketResetAtMS target.RocketTop1UserID = source.RocketTop1UserID target.RocketIgniterUserID = source.RocketIgniterUserID target.RocketID = source.RocketID target.RocketConfigVersion = source.RocketConfigVersion target.RocketPendingLaunches = source.RocketPendingLaunches target.RocketCurrentContributions = source.RocketCurrentContributions }