对接
This commit is contained in:
@@ -2,10 +2,14 @@ package gift
|
||||
|
||||
import (
|
||||
"context"
|
||||
kratoserrors "github.com/go-kratos/kratos/v3/errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
kratoserrors "github.com/go-kratos/kratos/v3/errors"
|
||||
|
||||
bizlive "kra/internal/biz/live"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -17,6 +21,24 @@ var (
|
||||
ErrSendNotAllowed = kratoserrors.BadRequest("GIFT_SEND_NOT_ALLOWED", "gift cannot be sent")
|
||||
)
|
||||
|
||||
// InsufficientBalanceError preserves the wallet snapshot needed by clients
|
||||
// to decide whether to open the recharge flow. It unwraps to the stable
|
||||
// ErrInsufficientBalance reason so existing errors.Is checks keep working.
|
||||
type InsufficientBalanceError struct {
|
||||
Required int64
|
||||
Balance int64
|
||||
Missing int64
|
||||
}
|
||||
|
||||
func (e *InsufficientBalanceError) Error() string {
|
||||
if e == nil {
|
||||
return ErrInsufficientBalance.Error()
|
||||
}
|
||||
return fmt.Sprintf("insufficient diamond balance: need %d, have %d, missing %d", e.Required, e.Balance, e.Missing)
|
||||
}
|
||||
|
||||
func (e *InsufficientBalanceError) Unwrap() error { return ErrInsufficientBalance }
|
||||
|
||||
type Gift struct {
|
||||
ID uint
|
||||
Name, Icon, Description string
|
||||
@@ -72,13 +94,21 @@ type Repo interface {
|
||||
type Notifier interface {
|
||||
GiftSent(context.Context, *Send)
|
||||
}
|
||||
type roomAvailability interface {
|
||||
IsLive(context.Context, uint) (bool, error)
|
||||
}
|
||||
type Usecase struct {
|
||||
repo Repo
|
||||
notifier Notifier
|
||||
rooms roomAvailability
|
||||
}
|
||||
|
||||
func NewUsecase(repo Repo, notifier Notifier) *Usecase {
|
||||
return &Usecase{repo: repo, notifier: notifier}
|
||||
func NewUsecase(repo Repo, notifier Notifier, rooms *bizlive.RoomUsecase) *Usecase {
|
||||
return newUsecase(repo, notifier, rooms)
|
||||
}
|
||||
|
||||
func newUsecase(repo Repo, notifier Notifier, rooms roomAvailability) *Usecase {
|
||||
return &Usecase{repo: repo, notifier: notifier, rooms: rooms}
|
||||
}
|
||||
func (uc *Usecase) List(ctx context.Context, f Filter, o ListOptions) ([]*Gift, int64, error) {
|
||||
f.Name = strings.TrimSpace(f.Name)
|
||||
@@ -115,6 +145,13 @@ func (uc *Usecase) Send(ctx context.Context, userID, roomID, giftID uint, quanti
|
||||
if userID == 0 || roomID == 0 || giftID == 0 || quantity < 1 || quantity > 9999 {
|
||||
return nil, ErrSendNotAllowed
|
||||
}
|
||||
if uc.rooms == nil {
|
||||
return nil, ErrSendNotAllowed
|
||||
}
|
||||
live, err := uc.rooms.IsLive(ctx, roomID)
|
||||
if err != nil || !live {
|
||||
return nil, ErrSendNotAllowed
|
||||
}
|
||||
result, err := uc.repo.Send(ctx, userID, roomID, giftID, quantity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package gift
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type sendRepoStub struct {
|
||||
Repo
|
||||
called bool
|
||||
}
|
||||
|
||||
func (r *sendRepoStub) Send(_ context.Context, userID, roomID, giftID uint, quantity int) (*Send, error) {
|
||||
r.called = true
|
||||
return &Send{UserID: userID, RoomID: &roomID, GiftID: &giftID, Quantity: quantity}, nil
|
||||
}
|
||||
|
||||
type availabilityStub struct {
|
||||
live bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (s availabilityStub) IsLive(context.Context, uint) (bool, error) { return s.live, s.err }
|
||||
|
||||
func TestSendRejectsOfflineRoomBeforeWalletMutation(t *testing.T) {
|
||||
repo := &sendRepoStub{}
|
||||
uc := newUsecase(repo, nil, availabilityStub{live: false})
|
||||
|
||||
if _, err := uc.Send(context.Background(), 1, 7, 3, 2); !errors.Is(err, ErrSendNotAllowed) {
|
||||
t.Fatalf("offline send error = %v", err)
|
||||
}
|
||||
if repo.called {
|
||||
t.Fatal("offline room reached gift repository")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendAcceptsCustomQuantityForLiveRoom(t *testing.T) {
|
||||
repo := &sendRepoStub{}
|
||||
uc := newUsecase(repo, nil, availabilityStub{live: true})
|
||||
|
||||
item, err := uc.Send(context.Background(), 1, 7, 3, 37)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !repo.called || item.Quantity != 37 {
|
||||
t.Fatalf("custom quantity result = %+v called=%v", item, repo.called)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package live
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -22,6 +24,7 @@ var (
|
||||
ErrCategoryDisabled = kratoserrors.BadRequest("LIVE_CATEGORY_DISABLED", "category is disabled")
|
||||
ErrRoomCategoryLocked = kratoserrors.BadRequest("LIVE_ROOM_CATEGORY_LOCKED", "live room category cannot be changed while broadcasting")
|
||||
ErrRoomInvalidArgument = kratoserrors.BadRequest("LIVE_ROOM_INVALID_ARGUMENT", "invalid live room data")
|
||||
ErrRoomNotLive = kratoserrors.BadRequest("LIVE_ROOM_NOT_LIVE", "live room is not broadcasting")
|
||||
ErrAnchorNotFound = kratoserrors.BadRequest("LIVE_ANCHOR_NOT_FOUND", "anchor user not found")
|
||||
)
|
||||
|
||||
@@ -37,23 +40,26 @@ type Category struct {
|
||||
}
|
||||
|
||||
type Room struct {
|
||||
ID uint
|
||||
Name string
|
||||
CategoryID uint
|
||||
CategoryName string
|
||||
CoverURL string
|
||||
Intro string
|
||||
Sort int
|
||||
AnchorUserID uint
|
||||
AnchorName string
|
||||
AnchorAvatar string
|
||||
StreamURL string
|
||||
ReplayURL string
|
||||
Enabled bool
|
||||
Status string
|
||||
ViewerCount int
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ID uint
|
||||
Name string
|
||||
CategoryID uint
|
||||
CategoryName string
|
||||
CoverURL string
|
||||
Intro string
|
||||
Sort int
|
||||
AnchorUserID uint
|
||||
AnchorName string
|
||||
AnchorAvatar string
|
||||
StreamURL string
|
||||
ReplayURL string
|
||||
Enabled bool
|
||||
Status string
|
||||
ViewerCount int
|
||||
ViewerCountText string
|
||||
LikeCount int
|
||||
StartedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type CategoryFilter struct {
|
||||
@@ -62,12 +68,13 @@ type CategoryFilter struct {
|
||||
}
|
||||
|
||||
type RoomFilter struct {
|
||||
Name string
|
||||
AnchorName string
|
||||
CategoryID uint
|
||||
Status string
|
||||
Enabled *bool
|
||||
CreatedAt []time.Time
|
||||
Name string
|
||||
AnchorName string
|
||||
CategoryID uint
|
||||
Status string
|
||||
Enabled *bool
|
||||
CategoryEnabled *bool
|
||||
CreatedAt []time.Time
|
||||
}
|
||||
|
||||
type ListOptions struct {
|
||||
@@ -174,6 +181,17 @@ func (uc *RoomUsecase) listByRuntimeStatus(ctx context.Context, filter RoomFilte
|
||||
matched = append(matched, room)
|
||||
}
|
||||
}
|
||||
if strings.EqualFold(options.OrderBy, "viewer_count_asc") || strings.EqualFold(options.OrderBy, "viewer_count_desc") {
|
||||
sort.SliceStable(matched, func(i, j int) bool {
|
||||
if matched[i].ViewerCount == matched[j].ViewerCount {
|
||||
return matched[i].ID < matched[j].ID
|
||||
}
|
||||
if strings.EqualFold(options.OrderBy, "viewer_count_asc") {
|
||||
return matched[i].ViewerCount < matched[j].ViewerCount
|
||||
}
|
||||
return matched[i].ViewerCount > matched[j].ViewerCount
|
||||
})
|
||||
}
|
||||
total := int64(len(matched))
|
||||
if options.Offset >= len(matched) {
|
||||
return []*Room{}, total, nil
|
||||
@@ -196,6 +214,32 @@ func (uc *RoomUsecase) Get(ctx context.Context, id uint) (*Room, error) {
|
||||
return uc.attachRuntime(ctx, []*Room{room})[0], nil
|
||||
}
|
||||
|
||||
func (uc *RoomUsecase) GetPublic(ctx context.Context, id uint) (*Room, error) {
|
||||
room, err := uc.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !room.Enabled || room.Status != RoomStatusLive {
|
||||
return nil, ErrRoomNotLive
|
||||
}
|
||||
category, err := uc.repo.GetCategory(ctx, room.CategoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !category.Enabled {
|
||||
return nil, ErrRoomNotLive
|
||||
}
|
||||
return room, nil
|
||||
}
|
||||
|
||||
func (uc *RoomUsecase) IsLive(ctx context.Context, id uint) (bool, error) {
|
||||
room, err := uc.Get(ctx, id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return room.Enabled && room.Status == RoomStatusLive, nil
|
||||
}
|
||||
|
||||
func (uc *RoomUsecase) Create(ctx context.Context, room *Room) (*Room, error) {
|
||||
if err := validateRoom(room); err != nil {
|
||||
return nil, err
|
||||
@@ -258,17 +302,41 @@ func (uc *RoomUsecase) attachRuntime(ctx context.Context, rooms []*Room) []*Room
|
||||
for _, room := range rooms {
|
||||
if !room.Enabled {
|
||||
room.Status = RoomStatusDisabled
|
||||
room.ViewerCountText = formatViewerCount(room.ViewerCount)
|
||||
continue
|
||||
}
|
||||
room.Status = RoomStatusOffline
|
||||
if stream := byID[roomStreamID(room.ID)]; stream != nil && stream.PublisherOnline {
|
||||
room.Status = RoomStatusLive
|
||||
room.ViewerCount = stream.SubscriberCount
|
||||
room.LikeCount = stream.LikeCount
|
||||
if !stream.StartedAt.IsZero() {
|
||||
startedAt := stream.StartedAt
|
||||
room.StartedAt = &startedAt
|
||||
}
|
||||
}
|
||||
room.ViewerCountText = formatViewerCount(room.ViewerCount)
|
||||
}
|
||||
return rooms
|
||||
}
|
||||
|
||||
func formatViewerCount(value int) string {
|
||||
if value < 1000 {
|
||||
if value < 0 {
|
||||
value = 0
|
||||
}
|
||||
return formatUint(uint(value))
|
||||
}
|
||||
if value >= 10000 {
|
||||
return formatDecimal(float64(value)/10000, "w")
|
||||
}
|
||||
return formatDecimal(float64(value)/1000, "k")
|
||||
}
|
||||
|
||||
func formatDecimal(value float64, suffix string) string {
|
||||
return fmt.Sprintf("%.1f%s", value, suffix)
|
||||
}
|
||||
|
||||
func roomStreamID(id uint) string { return "room-" + formatUint(id) }
|
||||
|
||||
func formatUint(value uint) string {
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type catalogRepoStub struct {
|
||||
categories []*Category
|
||||
rooms []*Room
|
||||
}
|
||||
|
||||
func (*catalogRepoStub) ListCategories(context.Context, CategoryFilter, ListOptions) ([]*Category, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
func (r *catalogRepoStub) GetCategory(_ context.Context, id uint) (*Category, error) {
|
||||
for _, category := range r.categories {
|
||||
if category.ID == id {
|
||||
copy := *category
|
||||
return ©, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrCategoryNotFound
|
||||
}
|
||||
func (*catalogRepoStub) CreateCategory(context.Context, *Category) (*Category, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (*catalogRepoStub) UpdateCategory(context.Context, *Category) (*Category, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (*catalogRepoStub) DeleteCategory(context.Context, uint) error { return nil }
|
||||
|
||||
func (r *catalogRepoStub) ListRooms(_ context.Context, filter RoomFilter, options ListOptions) ([]*Room, int64, error) {
|
||||
filtered := make([]*Room, 0, len(r.rooms))
|
||||
for _, room := range r.rooms {
|
||||
if filter.CategoryID > 0 && room.CategoryID != filter.CategoryID {
|
||||
continue
|
||||
}
|
||||
if filter.Enabled != nil && room.Enabled != *filter.Enabled {
|
||||
continue
|
||||
}
|
||||
if filter.CategoryEnabled != nil {
|
||||
category, err := r.GetCategory(context.Background(), room.CategoryID)
|
||||
if err != nil || category.Enabled != *filter.CategoryEnabled {
|
||||
continue
|
||||
}
|
||||
}
|
||||
copy := *room
|
||||
filtered = append(filtered, ©)
|
||||
}
|
||||
total := int64(len(filtered))
|
||||
if options.Offset >= len(filtered) {
|
||||
return []*Room{}, total, nil
|
||||
}
|
||||
end := options.Offset + options.Limit
|
||||
if end > len(filtered) {
|
||||
end = len(filtered)
|
||||
}
|
||||
return filtered[options.Offset:end], total, nil
|
||||
}
|
||||
|
||||
func (r *catalogRepoStub) GetRoom(_ context.Context, id uint) (*Room, error) {
|
||||
for _, room := range r.rooms {
|
||||
if room.ID == id {
|
||||
copy := *room
|
||||
return ©, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrRoomNotFound
|
||||
}
|
||||
|
||||
func (*catalogRepoStub) CreateRoom(context.Context, *Room) (*Room, error) { return nil, nil }
|
||||
func (*catalogRepoStub) UpdateRoom(context.Context, *Room) (*Room, error) { return nil, nil }
|
||||
func (*catalogRepoStub) DeleteRoom(context.Context, uint) error { return nil }
|
||||
func (*catalogRepoStub) UpdateRoomEnabled(context.Context, uint, bool) error {
|
||||
return nil
|
||||
}
|
||||
func (*catalogRepoStub) SaveRoomSort(context.Context, map[uint]int) error { return nil }
|
||||
|
||||
func TestRoomUsecaseFiltersSortsAndPaginatesLiveRooms(t *testing.T) {
|
||||
enabled := true
|
||||
catalog := &catalogRepoStub{rooms: []*Room{
|
||||
{ID: 1, CategoryID: 1, Enabled: true},
|
||||
{ID: 2, CategoryID: 1, Enabled: true},
|
||||
{ID: 3, CategoryID: 1, Enabled: true},
|
||||
{ID: 4, CategoryID: 2, Enabled: true},
|
||||
}}
|
||||
streams := NewStreamUsecase(&testRepo{streams: map[string]*Stream{
|
||||
"room-1": {StreamID: "room-1", PublisherOnline: true, SubscriberCount: 100},
|
||||
"room-2": {StreamID: "room-2", PublisherOnline: true, SubscriberCount: 5600},
|
||||
"room-4": {StreamID: "room-4", PublisherOnline: true, SubscriberCount: 10000},
|
||||
}})
|
||||
rooms := NewRoomUsecase(catalog, streams)
|
||||
|
||||
items, total, err := rooms.List(context.Background(), RoomFilter{CategoryID: 1, Status: RoomStatusLive, Enabled: &enabled}, ListOptions{Offset: 1, Limit: 1, OrderBy: "viewer_count_desc"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if total != 2 || len(items) != 1 || items[0].ID != 1 {
|
||||
t.Fatalf("live page total=%d items=%+v", total, items)
|
||||
}
|
||||
if items[0].ViewerCount != 100 || items[0].ViewerCountText != "100" || items[0].Status != RoomStatusLive {
|
||||
t.Fatalf("runtime fields = %+v", items[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoomUsecaseIsLiveRequiresEnabledOnlinePublisher(t *testing.T) {
|
||||
catalog := &catalogRepoStub{rooms: []*Room{
|
||||
{ID: 1, Enabled: true},
|
||||
{ID: 2, Enabled: true},
|
||||
{ID: 3, Enabled: false},
|
||||
}}
|
||||
streams := NewStreamUsecase(&testRepo{streams: map[string]*Stream{
|
||||
"room-1": {StreamID: "room-1", PublisherOnline: true},
|
||||
"room-3": {StreamID: "room-3", PublisherOnline: true},
|
||||
}})
|
||||
rooms := NewRoomUsecase(catalog, streams)
|
||||
|
||||
for roomID, want := range map[uint]bool{1: true, 2: false, 3: false} {
|
||||
got, err := rooms.IsLive(context.Background(), roomID)
|
||||
if err != nil {
|
||||
t.Fatalf("IsLive(%d) error = %v", roomID, err)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("IsLive(%d) = %v, want %v", roomID, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoomUsecaseGetPublicRequiresLiveRoomAndEnabledCategory(t *testing.T) {
|
||||
catalog := &catalogRepoStub{
|
||||
categories: []*Category{{ID: 1, Enabled: true}, {ID: 2, Enabled: false}},
|
||||
rooms: []*Room{
|
||||
{ID: 1, CategoryID: 1, Enabled: true},
|
||||
{ID: 2, CategoryID: 1, Enabled: true},
|
||||
{ID: 3, CategoryID: 1, Enabled: false},
|
||||
{ID: 4, CategoryID: 2, Enabled: true},
|
||||
},
|
||||
}
|
||||
streams := NewStreamUsecase(&testRepo{streams: map[string]*Stream{
|
||||
"room-1": {StreamID: "room-1", PublisherOnline: true},
|
||||
"room-3": {StreamID: "room-3", PublisherOnline: true},
|
||||
"room-4": {StreamID: "room-4", PublisherOnline: true},
|
||||
}})
|
||||
rooms := NewRoomUsecase(catalog, streams)
|
||||
|
||||
item, err := rooms.GetPublic(context.Background(), 1)
|
||||
if err != nil || item.ID != 1 || item.Status != RoomStatusLive {
|
||||
t.Fatalf("GetPublic(1) item=%+v err=%v", item, err)
|
||||
}
|
||||
for _, roomID := range []uint{2, 3, 4} {
|
||||
if _, err = rooms.GetPublic(context.Background(), roomID); !errors.Is(err, ErrRoomNotLive) {
|
||||
t.Errorf("GetPublic(%d) error=%v, want %v", roomID, err, ErrRoomNotLive)
|
||||
}
|
||||
if _, err = rooms.Get(context.Background(), roomID); err != nil {
|
||||
t.Errorf("admin Get(%d) error=%v", roomID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
kratoserrors "github.com/go-kratos/kratos/v3/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFollowNotFound = kratoserrors.NotFound("LIVE_FOLLOW_NOT_FOUND", "follow relation not found")
|
||||
ErrFollowInvalid = kratoserrors.BadRequest("LIVE_FOLLOW_INVALID_ARGUMENT", "invalid follow argument")
|
||||
)
|
||||
|
||||
type Follow struct {
|
||||
ID uint
|
||||
UserID uint
|
||||
RoomID uint
|
||||
AnchorUserID uint
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type FollowRepo interface {
|
||||
Get(context.Context, uint, uint) (*Follow, error)
|
||||
Create(context.Context, *Follow) (*Follow, error)
|
||||
Delete(context.Context, uint, uint) error
|
||||
}
|
||||
|
||||
type FollowUsecase struct{ repo FollowRepo }
|
||||
|
||||
func NewFollowUsecase(repo FollowRepo) *FollowUsecase { return &FollowUsecase{repo: repo} }
|
||||
|
||||
func (uc *FollowUsecase) Get(ctx context.Context, userID, roomID uint) (*Follow, error) {
|
||||
if userID == 0 || roomID == 0 {
|
||||
return nil, ErrFollowInvalid
|
||||
}
|
||||
return uc.repo.Get(ctx, userID, roomID)
|
||||
}
|
||||
|
||||
func (uc *FollowUsecase) Follow(ctx context.Context, userID, roomID uint) (*Follow, error) {
|
||||
if userID == 0 || roomID == 0 {
|
||||
return nil, ErrFollowInvalid
|
||||
}
|
||||
if existing, err := uc.repo.Get(ctx, userID, roomID); err == nil {
|
||||
return existing, nil
|
||||
} else if !errors.Is(err, ErrFollowNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
return uc.repo.Create(ctx, &Follow{UserID: userID, RoomID: roomID})
|
||||
}
|
||||
|
||||
func (uc *FollowUsecase) Unfollow(ctx context.Context, userID, roomID uint) error {
|
||||
if userID == 0 || roomID == 0 {
|
||||
return ErrFollowInvalid
|
||||
}
|
||||
return uc.repo.Delete(ctx, userID, roomID)
|
||||
}
|
||||
@@ -2,4 +2,4 @@ package live
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var ProviderSet = wire.NewSet(NewStreamUsecase, NewCategoryUsecase, NewRoomUsecase)
|
||||
var ProviderSet = wire.NewSet(NewStreamUsecase, NewCategoryUsecase, NewRoomUsecase, NewFollowUsecase)
|
||||
|
||||
@@ -27,6 +27,8 @@ type Stream struct {
|
||||
PublisherID uint
|
||||
PublisherOnline bool
|
||||
SubscriberCount int
|
||||
LikeCount int
|
||||
StartedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
@@ -37,6 +39,7 @@ type StreamRepo interface {
|
||||
UnregisterPublisher(context.Context, string, uint) error
|
||||
RegisterSubscriber(context.Context, string, uint) (*Stream, error)
|
||||
UnregisterSubscriber(context.Context, string, uint) error
|
||||
Like(context.Context, string) (int, error)
|
||||
}
|
||||
|
||||
type StreamUsecase struct{ repo StreamRepo }
|
||||
@@ -79,6 +82,14 @@ func (uc *StreamUsecase) UnregisterSubscriber(ctx context.Context, streamID stri
|
||||
return uc.repo.UnregisterSubscriber(ctx, normalizeStreamID(streamID), userID)
|
||||
}
|
||||
|
||||
func (uc *StreamUsecase) Like(ctx context.Context, streamID string) (int, error) {
|
||||
streamID = normalizeStreamID(streamID)
|
||||
if streamID == "" {
|
||||
return 0, ErrInvalidStreamID
|
||||
}
|
||||
return uc.repo.Like(ctx, streamID)
|
||||
}
|
||||
|
||||
func normalizeStreamID(value string) string {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,15 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatViewerCount(t *testing.T) {
|
||||
tests := map[int]string{0: "0", 999: "999", 1000: "1.0k", 5600: "5.6k", 10000: "1.0w", 12500: "1.2w"}
|
||||
for input, want := range tests {
|
||||
if got := formatViewerCount(input); got != want {
|
||||
t.Fatalf("formatViewerCount(%d) = %q, want %q", input, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type testRepo struct {
|
||||
streams map[string]*Stream
|
||||
}
|
||||
@@ -41,6 +50,7 @@ func (r *testRepo) RegisterSubscriber(context.Context, string, uint) (*Stream, e
|
||||
return nil, nil
|
||||
}
|
||||
func (r *testRepo) UnregisterSubscriber(context.Context, string, uint) error { return nil }
|
||||
func (r *testRepo) Like(context.Context, string) (int, error) { return 0, nil }
|
||||
|
||||
func TestStreamUsecaseValidatesAndDelegates(t *testing.T) {
|
||||
repo := &testRepo{streams: make(map[string]*Stream)}
|
||||
|
||||
@@ -53,6 +53,7 @@ type Repo interface {
|
||||
ListRecent(context.Context, uint, int) ([]*Message, error)
|
||||
CreateChat(context.Context, uint, uint, string) (*Message, error)
|
||||
CreateGift(context.Context, *GiftSnapshot) (*Message, error)
|
||||
Clear(context.Context, uint) error
|
||||
}
|
||||
|
||||
type Usecase struct{ repo Repo }
|
||||
@@ -83,3 +84,10 @@ func (uc *Usecase) RecordGift(ctx context.Context, gift *GiftSnapshot) (*Message
|
||||
}
|
||||
return uc.repo.CreateGift(ctx, gift)
|
||||
}
|
||||
|
||||
func (uc *Usecase) Clear(ctx context.Context, roomID uint) error {
|
||||
if roomID == 0 {
|
||||
return ErrRoomNotFound
|
||||
}
|
||||
return uc.repo.Clear(ctx, roomID)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ type Account struct {
|
||||
}
|
||||
|
||||
type RegisterInput struct {
|
||||
Username, Password, ConfirmPassword string
|
||||
Username, Nickname, Password, ConfirmPassword string
|
||||
}
|
||||
|
||||
type LoginInput struct {
|
||||
@@ -84,6 +84,12 @@ func (uc *Usecase) Register(ctx context.Context, in *RegisterInput, ip, agent st
|
||||
if n := len([]rune(in.Username)); n < 3 || n > 32 || in.Password == "" || in.Password != in.ConfirmPassword {
|
||||
return nil, ErrInvalidAccount
|
||||
}
|
||||
in.Nickname = strings.TrimSpace(in.Nickname)
|
||||
if in.Nickname == "" {
|
||||
in.Nickname = in.Username
|
||||
} else if n := len([]rune(in.Nickname)); n < 2 || n > 16 {
|
||||
return nil, ErrInvalidAccount
|
||||
}
|
||||
config, err := uc.security.Current(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -95,7 +101,7 @@ func (uc *Usecase) Register(ctx context.Context, in *RegisterInput, ip, agent st
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account, err := uc.repo.Register(ctx, &Account{Username: in.Username, Nickname: in.Username, AuthorityID: ClientAuthorityID, Enabled: true}, string(hash), InitialDiamonds)
|
||||
account, err := uc.repo.Register(ctx, &Account{Username: in.Username, Nickname: in.Nickname, AuthorityID: ClientAuthorityID, Enabled: true}, string(hash), InitialDiamonds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package liveclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz/system"
|
||||
)
|
||||
|
||||
type registerRepoStub struct {
|
||||
Repo
|
||||
account *Account
|
||||
}
|
||||
|
||||
func (r *registerRepoStub) Register(_ context.Context, account *Account, _ string, _ int64) (*Account, error) {
|
||||
copy := *account
|
||||
copy.UserID = 1
|
||||
copy.UUID = "client-uuid"
|
||||
copy.CreatedAt = time.Now()
|
||||
r.account = ©
|
||||
return ©, nil
|
||||
}
|
||||
|
||||
type registerSecurityRepoStub struct{ system.SecurityRepo }
|
||||
|
||||
func (*registerSecurityRepoStub) SecurityConfig(context.Context) (*system.SecurityConfig, error) {
|
||||
return &system.SecurityConfig{PwdMinLength: 8}, nil
|
||||
}
|
||||
|
||||
type registerSettingsStub struct{ system.RuntimeSettings }
|
||||
|
||||
func (*registerSettingsStub) UseMultipoint() bool { return false }
|
||||
|
||||
type registerIssuerStub struct{ system.TokenIssuer }
|
||||
|
||||
func (*registerIssuerStub) IssueToken(*system.User, uint, bool, time.Duration) (*system.IssuedToken, error) {
|
||||
return &system.IssuedToken{Value: "token", ExpiresAt: time.Now().Add(time.Hour), TTL: time.Hour}, nil
|
||||
}
|
||||
|
||||
func (*registerIssuerStub) ReissueToken(claims *system.AuthClaims, _ uint) (*system.IssuedToken, error) {
|
||||
return &system.IssuedToken{Value: "token", ExpiresAt: claims.ExpiresAt, TTL: time.Until(claims.ExpiresAt)}, nil
|
||||
}
|
||||
|
||||
func (*registerIssuerStub) ParseToken(string) (*system.AuthClaims, error) { return nil, nil }
|
||||
|
||||
func newRegisterUsecase(repo *registerRepoStub) *Usecase {
|
||||
security := system.NewSecurityUsecase(®isterSecurityRepoStub{}, nil, ®isterSettingsStub{}, nil)
|
||||
auth := system.NewAuthenticationUsecase(nil, security, ®isterIssuerStub{}, nil)
|
||||
return NewUsecase(repo, security, auth, nil)
|
||||
}
|
||||
|
||||
func TestRegisterUsesTrimmedNickname(t *testing.T) {
|
||||
repo := ®isterRepoStub{}
|
||||
uc := newRegisterUsecase(repo)
|
||||
result, err := uc.Register(context.Background(), &RegisterInput{
|
||||
Username: "viewer",
|
||||
Nickname: " 直播用户 ",
|
||||
Password: "password",
|
||||
ConfirmPassword: "password",
|
||||
}, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Account.Nickname != "直播用户" || repo.account.Nickname != "直播用户" {
|
||||
t.Fatalf("nickname = %q, repo nickname = %q", result.Account.Nickname, repo.account.Nickname)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterDefaultsNicknameToUsername(t *testing.T) {
|
||||
repo := ®isterRepoStub{}
|
||||
uc := newRegisterUsecase(repo)
|
||||
result, err := uc.Register(context.Background(), &RegisterInput{
|
||||
Username: "viewer",
|
||||
Password: "password",
|
||||
ConfirmPassword: "password",
|
||||
}, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Account.Nickname != "viewer" {
|
||||
t.Fatalf("nickname = %q, want username fallback", result.Account.Nickname)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterRejectsNicknameOutsideRange(t *testing.T) {
|
||||
for _, nickname := range []string{"A", "12345678901234567"} {
|
||||
t.Run(nickname, func(t *testing.T) {
|
||||
repo := ®isterRepoStub{}
|
||||
uc := newRegisterUsecase(repo)
|
||||
_, err := uc.Register(context.Background(), &RegisterInput{
|
||||
Username: "viewer",
|
||||
Nickname: nickname,
|
||||
Password: "password",
|
||||
ConfirmPassword: "password",
|
||||
}, "", "")
|
||||
if !errors.Is(err, ErrInvalidAccount) {
|
||||
t.Fatalf("error = %v, want ErrInvalidAccount", err)
|
||||
}
|
||||
if repo.account != nil {
|
||||
t.Fatal("repository called for invalid nickname")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user