diff --git a/services/wallet-service/internal/service/wallet/usecase/gift/service.go b/services/wallet-service/internal/service/wallet/usecase/gift/service.go index aede63f9..daf2619f 100644 --- a/services/wallet-service/internal/service/wallet/usecase/gift/service.go +++ b/services/wallet-service/internal/service/wallet/usecase/gift/service.go @@ -37,7 +37,10 @@ func (s *Service) DebitRobotGift(ctx context.Context, command ledger.DebitGiftCo } func (s *Service) debitGift(ctx context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error) { - if command.CommandID == "" || command.RoomID == "" || command.SenderUserID <= 0 || command.TargetUserID <= 0 || command.GiftID == "" { + 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 { diff --git a/services/wallet-service/internal/service/wallet/usecase/gift/service_test.go b/services/wallet-service/internal/service/wallet/usecase/gift/service_test.go new file mode 100644 index 00000000..67dab08d --- /dev/null +++ b/services/wallet-service/internal/service/wallet/usecase/gift/service_test.go @@ -0,0 +1,65 @@ +package gift + +import ( + "context" + "testing" + + "hyapp/pkg/xerr" + "hyapp/services/wallet-service/internal/domain/ledger" +) + +type fakeGiftLedgerStore struct { + debitCalls int + lastDebit ledger.DebitGiftCommand +} + +func (f *fakeGiftLedgerStore) DebitGift(_ context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error) { + f.debitCalls++ + f.lastDebit = command + return ledger.Receipt{TransactionID: "tx-direct"}, nil +} + +func (f *fakeGiftLedgerStore) BatchDebitGift(_ context.Context, _ ledger.BatchDebitGiftCommand) (ledger.BatchGiftReceipt, error) { + return ledger.BatchGiftReceipt{}, nil +} + +func TestDebitGiftAllowsDirectGiftWithoutRoomID(t *testing.T) { + store := &fakeGiftLedgerStore{} + svc := New(store) + + _, err := svc.DebitGift(context.Background(), ledger.DebitGiftCommand{ + AppCode: "lalu", + CommandID: "cmd-direct-gift", + DirectGift: true, + SenderUserID: 10001, + TargetUserID: 10002, + GiftID: "rose", + GiftCount: 1, + }) + if err != nil { + t.Fatalf("direct gift without room_id must reach repository: %v", err) + } + if store.debitCalls != 1 || !store.lastDebit.DirectGift || store.lastDebit.RoomID != "" { + t.Fatalf("direct gift command was not preserved: calls=%d command=%+v", store.debitCalls, store.lastDebit) + } +} + +func TestDebitGiftRequiresRoomIDForRoomGift(t *testing.T) { + store := &fakeGiftLedgerStore{} + svc := New(store) + + _, err := svc.DebitGift(context.Background(), ledger.DebitGiftCommand{ + AppCode: "lalu", + CommandID: "cmd-room-gift", + SenderUserID: 10001, + TargetUserID: 10002, + GiftID: "rose", + GiftCount: 1, + }) + if !xerr.IsCode(err, xerr.InvalidArgument) { + t.Fatalf("room gift without room_id must be invalid argument, got %v", err) + } + if store.debitCalls != 0 { + t.Fatalf("invalid room gift must not reach repository, calls=%d", store.debitCalls) + } +}