feat: live module and jenkins pipeline to harbor deploy
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
bizlive "kra/internal/biz/live"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
DB() *gorm.DB
|
||||
}
|
||||
|
||||
type categoryPO struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
Name string `gorm:"size:40;uniqueIndex"`
|
||||
Icon string `gorm:"size:500"`
|
||||
Sort int `gorm:"default:0;index"`
|
||||
Enabled bool `gorm:"default:true;index"`
|
||||
}
|
||||
|
||||
func (categoryPO) TableName() string { return "live_categories" }
|
||||
|
||||
type roomPO struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time `gorm:"index"`
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
Name string `gorm:"size:120;index"`
|
||||
CategoryID uint `gorm:"index"`
|
||||
CoverURL string `gorm:"size:1000"`
|
||||
Intro string `gorm:"size:500"`
|
||||
Sort int `gorm:"default:0;index"`
|
||||
AnchorUserID uint `gorm:"index"`
|
||||
StreamURL string `gorm:"size:1000"`
|
||||
ReplayURL string `gorm:"size:1000"`
|
||||
Enabled bool `gorm:"default:true;index"`
|
||||
}
|
||||
|
||||
func (roomPO) TableName() string { return "live_rooms" }
|
||||
|
||||
type roomRow struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt
|
||||
Name string
|
||||
CategoryID uint
|
||||
CoverURL string
|
||||
Intro string
|
||||
Sort int
|
||||
AnchorUserID uint
|
||||
StreamURL string
|
||||
ReplayURL string
|
||||
Enabled bool
|
||||
CategoryName string `gorm:"column:category_name"`
|
||||
AnchorName string `gorm:"column:anchor_name"`
|
||||
AnchorAvatar string `gorm:"column:anchor_avatar"`
|
||||
}
|
||||
|
||||
type categoryRow struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt
|
||||
Name string
|
||||
Icon string
|
||||
Sort int
|
||||
Enabled bool
|
||||
RoomCount int64 `gorm:"column:room_count"`
|
||||
}
|
||||
|
||||
type catalogRepo struct{ data Provider }
|
||||
|
||||
func NewCatalogRepo(data Provider) *catalogRepo { return &catalogRepo{data: data} }
|
||||
|
||||
func (r *catalogRepo) ListCategories(ctx context.Context, filter bizlive.CategoryFilter, options bizlive.ListOptions) ([]*bizlive.Category, int64, error) {
|
||||
db := r.db(ctx)
|
||||
if db == nil {
|
||||
return nil, 0, gorm.ErrInvalidDB
|
||||
}
|
||||
query := db.Model(&categoryPO{})
|
||||
if filter.Name != "" {
|
||||
query = query.Where("live_categories.name LIKE ?", "%"+filter.Name+"%")
|
||||
}
|
||||
if filter.Enabled != nil {
|
||||
query = query.Where("live_categories.enabled = ?", *filter.Enabled)
|
||||
}
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
order := categoryOrder(options.OrderBy)
|
||||
var rows []categoryRow
|
||||
err := query.
|
||||
Select("live_categories.*, COUNT(live_rooms.id) AS room_count").
|
||||
Joins("LEFT JOIN live_rooms ON live_rooms.category_id = live_categories.id AND live_rooms.deleted_at IS NULL AND live_rooms.enabled = ?", true).
|
||||
Group("live_categories.id").Order(order).Offset(options.Offset).Limit(options.Limit).Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items := make([]*bizlive.Category, 0, len(rows))
|
||||
for index := range rows {
|
||||
items = append(items, categoryToBiz(&rows[index]))
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (r *catalogRepo) GetCategory(ctx context.Context, id uint) (*bizlive.Category, error) {
|
||||
var row categoryRow
|
||||
err := r.db(ctx).Model(&categoryPO{}).
|
||||
Select("live_categories.*, COUNT(live_rooms.id) AS room_count").
|
||||
Joins("LEFT JOIN live_rooms ON live_rooms.category_id = live_categories.id AND live_rooms.deleted_at IS NULL AND live_rooms.enabled = ?", true).
|
||||
Where("live_categories.id = ?", id).Group("live_categories.id").Scan(&row).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if row.ID == 0 {
|
||||
return nil, bizlive.ErrCategoryNotFound
|
||||
}
|
||||
return categoryToBiz(&row), nil
|
||||
}
|
||||
|
||||
func (r *catalogRepo) CreateCategory(ctx context.Context, item *bizlive.Category) (*bizlive.Category, error) {
|
||||
po := newCategory(item)
|
||||
if err := r.db(ctx).Create(po).Error; err != nil {
|
||||
return nil, mapCategoryError(err)
|
||||
}
|
||||
return r.GetCategory(ctx, po.ID)
|
||||
}
|
||||
|
||||
func (r *catalogRepo) UpdateCategory(ctx context.Context, item *bizlive.Category) (*bizlive.Category, error) {
|
||||
result := r.db(ctx).Model(&categoryPO{}).Where("id = ?", item.ID).Updates(map[string]any{
|
||||
"name": item.Name, "icon": item.Icon, "sort": item.Sort, "enabled": item.Enabled,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return nil, mapCategoryError(result.Error)
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil, bizlive.ErrCategoryNotFound
|
||||
}
|
||||
return r.GetCategory(ctx, item.ID)
|
||||
}
|
||||
|
||||
func (r *catalogRepo) DeleteCategory(ctx context.Context, id uint) error {
|
||||
db := r.db(ctx)
|
||||
var count int64
|
||||
if err := db.Model(&roomPO{}).Where("category_id = ?", id).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return bizlive.ErrCategoryInUse
|
||||
}
|
||||
result := db.Delete(&categoryPO{}, id)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return bizlive.ErrCategoryNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *catalogRepo) ListRooms(ctx context.Context, filter bizlive.RoomFilter, options bizlive.ListOptions) ([]*bizlive.Room, int64, error) {
|
||||
db := r.db(ctx)
|
||||
query := db.Model(&roomPO{}).
|
||||
Joins("LEFT JOIN live_categories ON live_categories.id = live_rooms.category_id").
|
||||
Joins("LEFT JOIN sys_users ON sys_users.id = live_rooms.anchor_user_id AND sys_users.deleted_at IS NULL")
|
||||
if filter.Name != "" {
|
||||
query = query.Where("live_rooms.name LIKE ?", "%"+filter.Name+"%")
|
||||
}
|
||||
if filter.AnchorName != "" {
|
||||
value := "%" + filter.AnchorName + "%"
|
||||
query = query.Where("sys_users.nick_name LIKE ? OR sys_users.username LIKE ?", value, value)
|
||||
}
|
||||
if filter.CategoryID > 0 {
|
||||
query = query.Where("live_rooms.category_id = ?", filter.CategoryID)
|
||||
}
|
||||
if filter.Enabled != nil {
|
||||
query = query.Where("live_rooms.enabled = ?", *filter.Enabled)
|
||||
}
|
||||
if filter.Status == bizlive.RoomStatusDisabled {
|
||||
query = query.Where("live_rooms.enabled = ?", false)
|
||||
} else if filter.Status == bizlive.RoomStatusOffline || filter.Status == bizlive.RoomStatusLive {
|
||||
query = query.Where("live_rooms.enabled = ?", true)
|
||||
}
|
||||
if len(filter.CreatedAt) == 2 {
|
||||
query = query.Where("live_rooms.created_at BETWEEN ? AND ?", filter.CreatedAt[0], filter.CreatedAt[1])
|
||||
}
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var rows []roomRow
|
||||
err := query.Select("live_rooms.*, live_categories.name AS category_name, COALESCE(NULLIF(sys_users.nick_name, ''), sys_users.username) AS anchor_name, sys_users.header_img AS anchor_avatar").
|
||||
Order(roomOrder(options.OrderBy)).Offset(options.Offset).Limit(options.Limit).Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items := make([]*bizlive.Room, 0, len(rows))
|
||||
for index := range rows {
|
||||
items = append(items, roomToBiz(&rows[index]))
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (r *catalogRepo) GetRoom(ctx context.Context, id uint) (*bizlive.Room, error) {
|
||||
var row roomRow
|
||||
err := r.db(ctx).Model(&roomPO{}).
|
||||
Select("live_rooms.*, live_categories.name AS category_name, COALESCE(NULLIF(sys_users.nick_name, ''), sys_users.username) AS anchor_name, sys_users.header_img AS anchor_avatar").
|
||||
Joins("LEFT JOIN live_categories ON live_categories.id = live_rooms.category_id").
|
||||
Joins("LEFT JOIN sys_users ON sys_users.id = live_rooms.anchor_user_id AND sys_users.deleted_at IS NULL").
|
||||
Where("live_rooms.id = ?", id).Scan(&row).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if row.ID == 0 {
|
||||
return nil, bizlive.ErrRoomNotFound
|
||||
}
|
||||
return roomToBiz(&row), nil
|
||||
}
|
||||
|
||||
func (r *catalogRepo) CreateRoom(ctx context.Context, item *bizlive.Room) (*bizlive.Room, error) {
|
||||
if err := r.validateRoomRelations(ctx, item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
po := newRoom(item)
|
||||
if err := r.db(ctx).Create(po).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.GetRoom(ctx, po.ID)
|
||||
}
|
||||
|
||||
func (r *catalogRepo) UpdateRoom(ctx context.Context, item *bizlive.Room) (*bizlive.Room, error) {
|
||||
if err := r.validateRoomRelations(ctx, item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := r.db(ctx).Model(&roomPO{}).Where("id = ?", item.ID).Updates(map[string]any{
|
||||
"name": item.Name, "category_id": item.CategoryID, "cover_url": item.CoverURL, "intro": item.Intro,
|
||||
"sort": item.Sort, "anchor_user_id": item.AnchorUserID, "stream_url": item.StreamURL, "replay_url": item.ReplayURL, "enabled": item.Enabled,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil, bizlive.ErrRoomNotFound
|
||||
}
|
||||
return r.GetRoom(ctx, item.ID)
|
||||
}
|
||||
|
||||
func (r *catalogRepo) DeleteRoom(ctx context.Context, id uint) error {
|
||||
result := r.db(ctx).Delete(&roomPO{}, id)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return bizlive.ErrRoomNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *catalogRepo) UpdateRoomEnabled(ctx context.Context, id uint, enabled bool) error {
|
||||
result := r.db(ctx).Model(&roomPO{}).Where("id = ?", id).Update("enabled", enabled)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return bizlive.ErrRoomNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *catalogRepo) SaveRoomSort(ctx context.Context, values map[uint]int) error {
|
||||
return r.db(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for id, sortValue := range values {
|
||||
if err := tx.Model(&roomPO{}).Where("id = ?", id).Update("sort", sortValue).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *catalogRepo) validateRoomRelations(ctx context.Context, item *bizlive.Room) error {
|
||||
db := r.db(ctx)
|
||||
var category categoryPO
|
||||
if err := db.First(&category, item.CategoryID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return bizlive.ErrCategoryNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !category.Enabled {
|
||||
return bizlive.ErrCategoryDisabled
|
||||
}
|
||||
var count int64
|
||||
if err := db.Table("sys_users").Where("id = ? AND deleted_at IS NULL", item.AnchorUserID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return bizlive.ErrAnchorNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *catalogRepo) db(ctx context.Context) *gorm.DB {
|
||||
if r == nil || r.data == nil || r.data.DB() == nil {
|
||||
return nil
|
||||
}
|
||||
return r.data.DB().WithContext(ctx)
|
||||
}
|
||||
|
||||
func newCategory(item *bizlive.Category) *categoryPO {
|
||||
return &categoryPO{Name: item.Name, Icon: item.Icon, Sort: item.Sort, Enabled: item.Enabled}
|
||||
}
|
||||
|
||||
func categoryToBiz(row *categoryRow) *bizlive.Category {
|
||||
return &bizlive.Category{ID: row.ID, Name: row.Name, Icon: row.Icon, Sort: row.Sort, Enabled: row.Enabled, RoomCount: row.RoomCount, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}
|
||||
}
|
||||
|
||||
func newRoom(item *bizlive.Room) *roomPO {
|
||||
return &roomPO{Name: item.Name, CategoryID: item.CategoryID, CoverURL: item.CoverURL, Intro: item.Intro, Sort: item.Sort, AnchorUserID: item.AnchorUserID, StreamURL: item.StreamURL, ReplayURL: item.ReplayURL, Enabled: item.Enabled}
|
||||
}
|
||||
|
||||
func roomToBiz(row *roomRow) *bizlive.Room {
|
||||
return &bizlive.Room{ID: row.ID, Name: row.Name, CategoryID: row.CategoryID, CategoryName: row.CategoryName, CoverURL: row.CoverURL, Intro: row.Intro, Sort: row.Sort, AnchorUserID: row.AnchorUserID, AnchorName: row.AnchorName, AnchorAvatar: row.AnchorAvatar, StreamURL: row.StreamURL, ReplayURL: row.ReplayURL, Enabled: row.Enabled, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt}
|
||||
}
|
||||
|
||||
func categoryOrder(value string) string {
|
||||
allowed := map[string]string{"sort_asc": "live_categories.sort ASC", "sort_desc": "live_categories.sort DESC", "created_at_asc": "live_categories.created_at ASC", "created_at_desc": "live_categories.created_at DESC"}
|
||||
if order := allowed[strings.ToLower(value)]; order != "" {
|
||||
return order
|
||||
}
|
||||
return "live_categories.sort ASC, live_categories.created_at DESC"
|
||||
}
|
||||
|
||||
func roomOrder(value string) string {
|
||||
allowed := map[string]string{
|
||||
"sort_asc": "live_rooms.sort ASC", "sort_desc": "live_rooms.sort DESC", "id_asc": "live_rooms.id ASC", "id_desc": "live_rooms.id DESC",
|
||||
"name_asc": "live_rooms.name ASC", "name_desc": "live_rooms.name DESC", "created_at_asc": "live_rooms.created_at ASC", "created_at_desc": "live_rooms.created_at DESC",
|
||||
}
|
||||
if order := allowed[strings.ToLower(value)]; order != "" {
|
||||
return order
|
||||
}
|
||||
return "live_rooms.sort ASC, live_rooms.created_at DESC"
|
||||
}
|
||||
|
||||
func mapCategoryError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
message := strings.ToLower(err.Error())
|
||||
if strings.Contains(message, "unique") || strings.Contains(message, "duplicate") {
|
||||
return bizlive.ErrCategoryNameExists
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
bizlive "kra/internal/biz/live"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type catalogTestProvider struct{ db *gorm.DB }
|
||||
|
||||
func (p catalogTestProvider) DB() *gorm.DB { return p.db }
|
||||
|
||||
func openCatalogTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = db.AutoMigrate(&categoryPO{}, &roomPO{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = db.Exec(`CREATE TABLE sys_users (
|
||||
id integer primary key,
|
||||
username text,
|
||||
nick_name text,
|
||||
header_img text,
|
||||
deleted_at datetime
|
||||
)`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestCatalogRepoScansCategoryAndRoomFields(t *testing.T) {
|
||||
db := openCatalogTestDB(t)
|
||||
if err := db.Exec(`INSERT INTO sys_users(id, username, nick_name, header_img) VALUES (2, 'anchor', 'Anchor', 'avatar.png')`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
category := categoryPO{Name: "Games", Icon: "game.png", Sort: 1, Enabled: true}
|
||||
if err := db.Create(&category).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
room := roomPO{Name: "Test Room", CategoryID: category.ID, CoverURL: "cover.png", Intro: "intro", Sort: 2, AnchorUserID: 2, Enabled: true}
|
||||
if err := db.Create(&room).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
repo := NewCatalogRepo(catalogTestProvider{db: db})
|
||||
categories, total, err := repo.ListCategories(context.Background(), bizlive.CategoryFilter{}, bizlive.ListOptions{Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if total != 1 || len(categories) != 1 || categories[0].ID != category.ID || categories[0].Name != category.Name || !categories[0].Enabled || categories[0].RoomCount != 1 {
|
||||
t.Fatalf("category scan result total=%d categories=%+v", total, categories)
|
||||
}
|
||||
|
||||
rooms, total, err := repo.ListRooms(context.Background(), bizlive.RoomFilter{}, bizlive.ListOptions{Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if total != 1 || len(rooms) != 1 || rooms[0].ID != room.ID || rooms[0].Name != room.Name || rooms[0].CategoryName != category.Name || rooms[0].AnchorName != "Anchor" || !rooms[0].Enabled {
|
||||
t.Fatalf("room list scan result total=%d rooms=%+v", total, rooms)
|
||||
}
|
||||
|
||||
got, err := repo.GetRoom(context.Background(), room.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.ID != room.ID || got.Name != room.Name || got.AnchorUserID != 2 || got.AnchorAvatar != "avatar.png" || !got.Enabled {
|
||||
t.Fatalf("room get scan result=%+v", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"kra/pkg/database/migration"
|
||||
)
|
||||
|
||||
func Migrations() []migration.Step {
|
||||
return []migration.Step{{ID: "202609190001_live_schema", Migrate: func(db *gorm.DB) error {
|
||||
if err := migration.CreateMissingTables(db, &categoryPO{}, &roomPO{}); err != nil {
|
||||
return err
|
||||
}
|
||||
var count int64
|
||||
if err := db.Model(&categoryPO{}).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
return db.Create(&[]categoryPO{{Name: "游戏竞技", Icon: "🎮", Sort: 1, Enabled: true}, {Name: "音乐区", Icon: "🎵", Sort: 2, Enabled: true}, {Name: "闲聊电台", Icon: "🌙", Sort: 3, Enabled: true}, {Name: "舞蹈才艺", Icon: "💃", Sort: 4, Enabled: true}, {Name: "娱乐直播", Icon: "🎪", Sort: 5, Enabled: true}}).Error
|
||||
}}}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
bizlive "kra/internal/biz/live"
|
||||
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewStreamRepo,
|
||||
NewCatalogRepo,
|
||||
wire.Bind(new(bizlive.CatalogRepo), new(*catalogRepo)),
|
||||
)
|
||||
@@ -0,0 +1,130 @@
|
||||
package live
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
bizlive "kra/internal/biz/live"
|
||||
)
|
||||
|
||||
type streamState struct {
|
||||
stream *bizlive.Stream
|
||||
users map[uint]struct{}
|
||||
}
|
||||
|
||||
type streamRepo struct {
|
||||
mu sync.RWMutex
|
||||
streams map[string]*streamState
|
||||
}
|
||||
|
||||
func NewStreamRepo() bizlive.StreamRepo {
|
||||
return &streamRepo{streams: make(map[string]*streamState)}
|
||||
}
|
||||
|
||||
func (r *streamRepo) Get(_ context.Context, streamID string) (*bizlive.Stream, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
state := r.streams[streamID]
|
||||
if state == nil {
|
||||
return &bizlive.Stream{StreamID: streamID, UpdatedAt: time.Now()}, nil
|
||||
}
|
||||
return clone(state.stream), nil
|
||||
}
|
||||
|
||||
func (r *streamRepo) List(_ context.Context) ([]*bizlive.Stream, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
items := make([]*bizlive.Stream, 0, len(r.streams))
|
||||
for _, state := range r.streams {
|
||||
if state.stream.PublisherOnline {
|
||||
items = append(items, clone(state.stream))
|
||||
}
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool { return items[i].StreamID < items[j].StreamID })
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *streamRepo) RegisterPublisher(_ context.Context, streamID string, userID uint) (*bizlive.Stream, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
state := r.streams[streamID]
|
||||
if state == nil {
|
||||
if len(r.streams) >= bizlive.MaxStreams {
|
||||
return nil, bizlive.ErrMaxStreams
|
||||
}
|
||||
state = &streamState{stream: &bizlive.Stream{StreamID: streamID}, users: make(map[uint]struct{})}
|
||||
r.streams[streamID] = state
|
||||
}
|
||||
if state.stream.PublisherOnline {
|
||||
return nil, bizlive.ErrPublisherExists
|
||||
}
|
||||
state.stream.PublisherID = userID
|
||||
state.stream.PublisherOnline = true
|
||||
state.stream.UpdatedAt = time.Now()
|
||||
return clone(state.stream), nil
|
||||
}
|
||||
|
||||
func (r *streamRepo) UnregisterPublisher(_ context.Context, streamID string, userID uint) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
state := r.streams[streamID]
|
||||
if state == nil || !state.stream.PublisherOnline || state.stream.PublisherID != userID {
|
||||
return bizlive.ErrPublisherNotFound
|
||||
}
|
||||
state.stream.PublisherID = 0
|
||||
state.stream.PublisherOnline = false
|
||||
state.stream.UpdatedAt = time.Now()
|
||||
if len(state.users) == 0 {
|
||||
delete(r.streams, streamID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *streamRepo) RegisterSubscriber(_ context.Context, streamID string, userID uint) (*bizlive.Stream, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
state := r.streams[streamID]
|
||||
if state == nil {
|
||||
if len(r.streams) >= bizlive.MaxStreams {
|
||||
return nil, bizlive.ErrMaxStreams
|
||||
}
|
||||
state = &streamState{stream: &bizlive.Stream{StreamID: streamID}, users: make(map[uint]struct{})}
|
||||
r.streams[streamID] = state
|
||||
}
|
||||
if _, exists := state.users[userID]; exists {
|
||||
return nil, bizlive.ErrSubscriberExists
|
||||
}
|
||||
state.users[userID] = struct{}{}
|
||||
state.stream.SubscriberCount = len(state.users)
|
||||
state.stream.UpdatedAt = time.Now()
|
||||
return clone(state.stream), nil
|
||||
}
|
||||
|
||||
func (r *streamRepo) UnregisterSubscriber(_ context.Context, streamID string, userID uint) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
state := r.streams[streamID]
|
||||
if state == nil {
|
||||
return bizlive.ErrSubscriberNotFound
|
||||
}
|
||||
if _, exists := state.users[userID]; !exists {
|
||||
return bizlive.ErrSubscriberNotFound
|
||||
}
|
||||
delete(state.users, userID)
|
||||
state.stream.SubscriberCount = len(state.users)
|
||||
state.stream.UpdatedAt = time.Now()
|
||||
if !state.stream.PublisherOnline && len(state.users) == 0 {
|
||||
delete(r.streams, streamID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func clone(stream *bizlive.Stream) *bizlive.Stream {
|
||||
if stream == nil {
|
||||
return nil
|
||||
}
|
||||
value := *stream
|
||||
return &value
|
||||
}
|
||||
Reference in New Issue
Block a user