// Package gift contains gift debit usecases. package gift import ( "context" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" "hyapp/services/wallet-service/internal/service/wallet/ports" ) // Service 校验礼物扣费命令,并把原子账务提交交给 repository。 type Service struct { // repository 只需要礼物账本端口,后续单测不用实现完整 wallet Repository。 repository ports.GiftLedgerStore } // New 创建礼物扣费用例。 func New(repository ports.GiftLedgerStore) *Service { return &Service{repository: repository} } // DebitGift 校验普通送礼扣费命令,并交给 repository 做原子落账和幂等。 func (s *Service) DebitGift(ctx context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error) { command.RobotGift = false return s.debitGift(ctx, command) } // DebitRobotGift 校验机器人房间专用送礼扣费命令,并强制使用 ROBOT_COIN 账务语义。 func (s *Service) DebitRobotGift(ctx context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error) { command.RobotGift = true command.TargetIsHost = false command.TargetHostRegionID = 0 command.TargetAgencyOwnerUserID = 0 return s.debitGift(ctx, command) } func (s *Service) debitGift(ctx context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error) { roomIDRequired := !command.DirectGift if command.CommandID == "" || (roomIDRequired && command.RoomID == "") || command.SenderUserID <= 0 || command.TargetUserID <= 0 || command.GiftID == "" { // 房间送礼必须带 RoomID,让 Room Cell、账务分录和房间 outbox 能共享同一个业务边界。 // 私聊送礼没有房间状态 owner;DirectGift 使用独立 biz_type 和 request hash 隔离幂等,因此允许 RoomID 为空。 return ledger.Receipt{}, xerr.New(xerr.InvalidArgument, "billing command is incomplete") } if command.GiftCount <= 0 { return ledger.Receipt{}, xerr.New(xerr.InvalidArgument, "gift_count must be positive") } if command.TargetIsHost && command.TargetHostRegionID <= 0 { // 主播周期钻石后续要按区域工资政策结算;只有 active host profile 且带区域时才允许入账。 return ledger.Receipt{}, xerr.New(xerr.InvalidArgument, "target_host_region_id is required") } if s == nil || s.repository == nil { return ledger.Receipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.DebitGift(ctx, command) } // BatchDebitGift 校验多目标送礼扣费命令,并要求 repository 在同一事务中完成全部目标结算。 func (s *Service) BatchDebitGift(ctx context.Context, command ledger.BatchDebitGiftCommand) (ledger.BatchGiftReceipt, error) { if command.CommandID == "" || command.RoomID == "" || command.SenderUserID <= 0 || command.GiftID == "" { return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "billing command is incomplete") } if command.GiftCount <= 0 { return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "gift_count must be positive") } if len(command.Targets) == 0 { return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "gift targets are required") } seen := make(map[int64]struct{}, len(command.Targets)) for _, target := range command.Targets { if target.CommandID == "" || target.TargetUserID <= 0 { return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "gift target is incomplete") } if _, exists := seen[target.TargetUserID]; exists { return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "gift target is duplicated") } seen[target.TargetUserID] = struct{}{} if target.TargetIsHost && target.TargetHostRegionID <= 0 { // 主播工资入账必须带该目标自己的主播区域,不能复用房间区域或其他目标快照。 return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "target_host_region_id is required") } } if s == nil || s.repository == nil { return ledger.BatchGiftReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.BatchDebitGift(ctx, command) }