package live import ( "context" "strings" "time" kratoserrors "github.com/go-kratos/kratos/v3/errors" ) const ( RoomStatusOffline = "offline" RoomStatusLive = "live" RoomStatusDisabled = "disabled" ) var ( ErrRoomNotFound = kratoserrors.NotFound("LIVE_ROOM_NOT_FOUND", "live room not found") ErrCategoryNotFound = kratoserrors.NotFound("LIVE_CATEGORY_NOT_FOUND", "live category not found") ErrCategoryNameExists = kratoserrors.BadRequest("LIVE_CATEGORY_NAME_EXISTS", "category name already exists") ErrCategoryInUse = kratoserrors.BadRequest("LIVE_CATEGORY_IN_USE", "category is still used by live rooms") 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") ErrAnchorNotFound = kratoserrors.BadRequest("LIVE_ANCHOR_NOT_FOUND", "anchor user not found") ) type Category struct { ID uint Name string Icon string Sort int Enabled bool RoomCount int64 CreatedAt time.Time UpdatedAt time.Time } 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 } type CategoryFilter struct { Name string Enabled *bool } type RoomFilter struct { Name string AnchorName string CategoryID uint Status string Enabled *bool CreatedAt []time.Time } type ListOptions struct { Offset int Limit int OrderBy string } type CatalogRepo interface { ListCategories(context.Context, CategoryFilter, ListOptions) ([]*Category, int64, error) GetCategory(context.Context, uint) (*Category, error) CreateCategory(context.Context, *Category) (*Category, error) UpdateCategory(context.Context, *Category) (*Category, error) DeleteCategory(context.Context, uint) error ListRooms(context.Context, RoomFilter, ListOptions) ([]*Room, int64, error) GetRoom(context.Context, uint) (*Room, error) CreateRoom(context.Context, *Room) (*Room, error) UpdateRoom(context.Context, *Room) (*Room, error) DeleteRoom(context.Context, uint) error UpdateRoomEnabled(context.Context, uint, bool) error SaveRoomSort(context.Context, map[uint]int) error } type CategoryUsecase struct{ repo CatalogRepo } func NewCategoryUsecase(repo CatalogRepo) *CategoryUsecase { return &CategoryUsecase{repo: repo} } func (uc *CategoryUsecase) List(ctx context.Context, filter CategoryFilter, options ListOptions) ([]*Category, int64, error) { filter.Name = strings.TrimSpace(filter.Name) return uc.repo.ListCategories(ctx, filter, normalizeListOptions(options)) } func (uc *CategoryUsecase) Get(ctx context.Context, id uint) (*Category, error) { if id == 0 { return nil, ErrCategoryNotFound } return uc.repo.GetCategory(ctx, id) } func (uc *CategoryUsecase) Create(ctx context.Context, item *Category) (*Category, error) { if err := validateCategory(item); err != nil { return nil, err } return uc.repo.CreateCategory(ctx, item) } func (uc *CategoryUsecase) Update(ctx context.Context, item *Category) (*Category, error) { if item == nil || item.ID == 0 { return nil, ErrCategoryNotFound } if err := validateCategory(item); err != nil { return nil, err } return uc.repo.UpdateCategory(ctx, item) } func (uc *CategoryUsecase) Delete(ctx context.Context, id uint) error { if id == 0 { return ErrCategoryNotFound } return uc.repo.DeleteCategory(ctx, id) } type RoomUsecase struct { repo CatalogRepo streams *StreamUsecase } func NewRoomUsecase(repo CatalogRepo, streams *StreamUsecase) *RoomUsecase { return &RoomUsecase{repo: repo, streams: streams} } func (uc *RoomUsecase) List(ctx context.Context, filter RoomFilter, options ListOptions) ([]*Room, int64, error) { filter.Name = strings.TrimSpace(filter.Name) filter.AnchorName = strings.TrimSpace(filter.AnchorName) options = normalizeListOptions(options) if filter.Status == RoomStatusLive || filter.Status == RoomStatusOffline { return uc.listByRuntimeStatus(ctx, filter, options) } rooms, total, err := uc.repo.ListRooms(ctx, filter, options) if err != nil { return nil, 0, err } return uc.attachRuntime(ctx, rooms), total, nil } func (uc *RoomUsecase) listByRuntimeStatus(ctx context.Context, filter RoomFilter, options ListOptions) ([]*Room, int64, error) { all := make([]*Room, 0) for offset := 0; ; offset += 100 { batch, total, err := uc.repo.ListRooms(ctx, filter, ListOptions{Offset: offset, Limit: 100, OrderBy: options.OrderBy}) if err != nil { return nil, 0, err } all = append(all, batch...) if len(all) >= int(total) || len(batch) == 0 { break } } all = uc.attachRuntime(ctx, all) matched := make([]*Room, 0, len(all)) for _, room := range all { if room.Status == filter.Status { matched = append(matched, room) } } total := int64(len(matched)) if options.Offset >= len(matched) { return []*Room{}, total, nil } end := options.Offset + options.Limit if end > len(matched) { end = len(matched) } return matched[options.Offset:end], total, nil } func (uc *RoomUsecase) Get(ctx context.Context, id uint) (*Room, error) { if id == 0 { return nil, ErrRoomNotFound } room, err := uc.repo.GetRoom(ctx, id) if err != nil { return nil, err } return uc.attachRuntime(ctx, []*Room{room})[0], nil } func (uc *RoomUsecase) Create(ctx context.Context, room *Room) (*Room, error) { if err := validateRoom(room); err != nil { return nil, err } return uc.repo.CreateRoom(ctx, room) } func (uc *RoomUsecase) Update(ctx context.Context, room *Room) (*Room, error) { if room == nil || room.ID == 0 { return nil, ErrRoomNotFound } if err := validateRoom(room); err != nil { return nil, err } current, err := uc.Get(ctx, room.ID) if err != nil { return nil, err } if current.Status == RoomStatusLive && current.CategoryID != room.CategoryID { return nil, ErrRoomCategoryLocked } return uc.repo.UpdateRoom(ctx, room) } func (uc *RoomUsecase) Delete(ctx context.Context, id uint) error { if id == 0 { return ErrRoomNotFound } return uc.repo.DeleteRoom(ctx, id) } func (uc *RoomUsecase) SetEnabled(ctx context.Context, id uint, enabled bool) error { if id == 0 { return ErrRoomNotFound } return uc.repo.UpdateRoomEnabled(ctx, id, enabled) } func (uc *RoomUsecase) SaveSort(ctx context.Context, values map[uint]int) error { if len(values) == 0 { return nil } for id, sortValue := range values { if id == 0 || sortValue < 0 || sortValue > 9999 { return ErrRoomInvalidArgument } } return uc.repo.SaveRoomSort(ctx, values) } func (uc *RoomUsecase) attachRuntime(ctx context.Context, rooms []*Room) []*Room { streams, err := uc.streams.List(ctx) if err != nil { return rooms } byID := make(map[string]*Stream, len(streams)) for _, stream := range streams { byID[stream.StreamID] = stream } for _, room := range rooms { if !room.Enabled { room.Status = RoomStatusDisabled continue } room.Status = RoomStatusOffline if stream := byID[roomStreamID(room.ID)]; stream != nil && stream.PublisherOnline { room.Status = RoomStatusLive room.ViewerCount = stream.SubscriberCount } } return rooms } func roomStreamID(id uint) string { return "room-" + formatUint(id) } func formatUint(value uint) string { const digits = "0123456789" if value == 0 { return "0" } var buf [20]byte index := len(buf) for value > 0 { index-- buf[index] = digits[value%10] value /= 10 } return string(buf[index:]) } func normalizeListOptions(options ListOptions) ListOptions { if options.Offset < 0 { options.Offset = 0 } if options.Limit <= 0 || options.Limit > 100 { options.Limit = 10 } return options } func validateCategory(item *Category) error { if item == nil { return ErrRoomInvalidArgument } item.Name = strings.TrimSpace(item.Name) item.Icon = strings.TrimSpace(item.Icon) if length := len([]rune(item.Name)); length < 1 || length > 10 || item.Sort < 0 || item.Sort > 9999 { return ErrRoomInvalidArgument } return nil } func validateRoom(room *Room) error { if room == nil { return ErrRoomInvalidArgument } room.Name = strings.TrimSpace(room.Name) room.Intro = strings.TrimSpace(room.Intro) room.CoverURL = strings.TrimSpace(room.CoverURL) room.StreamURL = strings.TrimSpace(room.StreamURL) room.ReplayURL = strings.TrimSpace(room.ReplayURL) if nameLength := len([]rune(room.Name)); nameLength < 1 || nameLength > 30 { return ErrRoomInvalidArgument } if len([]rune(room.Intro)) > 60 || room.CategoryID == 0 || room.AnchorUserID == 0 || room.CoverURL == "" || room.Sort < 0 || room.Sort > 9999 { return ErrRoomInvalidArgument } if room.StreamURL != "" && !strings.HasPrefix(room.StreamURL, "http://") && !strings.HasPrefix(room.StreamURL, "https://") { return ErrRoomInvalidArgument } return nil }