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:"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:"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 AND live_categories.deleted_at IS NULL"). Joins("LEFT JOIN sys_users ON sys_users.id = live_rooms.anchor_user_id AND sys_users.deleted_at IS NULL") if filter.Name != "" { value := "%" + filter.Name + "%" query = query.Where("live_rooms.name LIKE ? OR CAST(live_rooms.id AS CHAR) LIKE ?", value, value) } 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.CategoryEnabled != nil { query = query.Where("live_categories.enabled = ?", *filter.CategoryEnabled) } 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 AND live_categories.deleted_at IS NULL"). 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 }