Files
nl-blogs/server/repositories/video_repository.go
2026-07-06 08:19:47 +08:00

481 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package repositories
import (
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
"gorm.io/gorm"
)
// ========== 视频分类 ==========
func GetVideoCategories() ([]models.VideoCategory, error) {
var list []models.VideoCategory
err := config.DB.Model(&models.VideoCategory{}).
Where("deleted_at = ?", 0).
Order("sort_order ASC, id ASC").
Find(&list).Error
return list, err
}
func GetVideoCategoryByID(id uint) (*models.VideoCategory, error) {
var item models.VideoCategory
err := config.DB.Model(&models.VideoCategory{}).
Where("id = ? AND deleted_at = ?", id, 0).
First(&item).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &item, err
}
func CreateVideoCategory(item *models.VideoCategory) error {
return config.DB.Create(item).Error
}
func UpdateVideoCategory(item *models.VideoCategory) error {
return config.DB.Model(&models.VideoCategory{}).
Where("id = ? AND deleted_at = ?", item.ID, 0).
Updates(map[string]interface{}{
"name": item.Name,
"slug": item.Slug,
"description": item.Description,
"sort_order": item.SortOrder,
"updated_at": time.Now().Unix(),
}).Error
}
func DeleteVideoCategory(id uint) error {
return config.DB.Model(&models.VideoCategory{}).
Where("id = ?", id).
Update("deleted_at", time.Now().Unix()).Error
}
func BuildVideoCategoryResponse(item *models.VideoCategory) models.VideoCategoryResponse {
var videoCount, albumCount int64
config.DB.Model(&models.Video{}).
Where("category_id = ? AND deleted_at = ? AND is_published = ?", item.ID, 0, 1).
Count(&videoCount)
config.DB.Model(&models.VideoAlbum{}).
Where("category_id = ? AND deleted_at = ? AND is_active = ?", item.ID, 0, 1).
Count(&albumCount)
return models.VideoCategoryResponse{
ID: item.ID,
Name: item.Name,
Slug: item.Slug,
Description: item.Description,
SortOrder: item.SortOrder,
CreatedAt: formatTimestamp(item.CreatedAt),
UpdatedAt: formatTimestamp(item.UpdatedAt),
VideoCount: videoCount,
AlbumCount: albumCount,
}
}
func BuildVideoCategoriesResponse(list []models.VideoCategory) []models.VideoCategoryResponse {
res := make([]models.VideoCategoryResponse, 0, len(list))
for i := range list {
res = append(res, BuildVideoCategoryResponse(&list[i]))
}
return res
}
// ========== 视频专辑 ==========
func GetVideoAlbums(categoryID uint) ([]models.VideoAlbum, error) {
var list []models.VideoAlbum
q := config.DB.Model(&models.VideoAlbum{}).Where("deleted_at = ?", 0)
if categoryID > 0 {
q = q.Where("category_id = ?", categoryID)
}
err := q.Order("sort_order ASC, created_at DESC").Find(&list).Error
return list, err
}
func GetActiveVideoAlbums(categoryID uint) ([]models.VideoAlbum, error) {
var list []models.VideoAlbum
q := config.DB.Model(&models.VideoAlbum{}).Where("deleted_at = ? AND is_active = ?", 0, 1)
if categoryID > 0 {
q = q.Where("category_id = ?", categoryID)
}
err := q.Order("sort_order ASC, created_at DESC").Find(&list).Error
return list, err
}
func GetVideoAlbumByID(id uint) (*models.VideoAlbum, error) {
var item models.VideoAlbum
err := config.DB.Model(&models.VideoAlbum{}).
Where("id = ? AND deleted_at = ?", id, 0).
First(&item).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &item, err
}
func CreateVideoAlbum(item *models.VideoAlbum) error {
return config.DB.Create(item).Error
}
func UpdateVideoAlbum(item *models.VideoAlbum) error {
return config.DB.Model(&models.VideoAlbum{}).
Where("id = ? AND deleted_at = ?", item.ID, 0).
Updates(map[string]interface{}{
"name": item.Name,
"description": item.Description,
"cover": item.Cover,
"category_id": item.CategoryID,
"is_active": item.IsActive,
"sort_order": item.SortOrder,
"updated_at": time.Now().Unix(),
}).Error
}
func DeleteVideoAlbum(id uint) error {
return config.DB.Model(&models.VideoAlbum{}).
Where("id = ?", id).
Update("deleted_at", time.Now().Unix()).Error
}
// GetAlbumVideoCount 统计专辑内视频数量publishedOnly 为 true 时仅统计已发布视频
func GetAlbumVideoCount(albumID uint, publishedOnly bool) int64 {
var count int64
q := config.DB.Model(&models.AlbumVideo{}).
Joins("JOIN videos v ON v.id = album_videos.video_id").
Where("album_videos.album_id = ? AND v.deleted_at = ?", albumID, 0)
if publishedOnly {
q = q.Where("v.is_published = ?", 1)
}
q.Count(&count)
return count
}
// GetAlbumLatestVideoUpdatedAt 获取专辑内视频最新更新时间戳
func GetAlbumLatestVideoUpdatedAt(albumID uint, publishedOnly bool) int64 {
var ts int64
q := config.DB.Model(&models.Video{}).
Select("COALESCE(MAX(videos.updated_at), 0)").
Joins("JOIN album_videos av ON av.video_id = videos.id").
Where("av.album_id = ? AND videos.deleted_at = ?", albumID, 0)
if publishedOnly {
q = q.Where("videos.is_published = ?", 1)
}
q.Scan(&ts)
return ts
}
// GetAlbumPreviewVideos 获取专辑预览视频列表(按 sort_order最多 limit 条)
func GetAlbumPreviewVideos(albumID uint, limit int, publishedOnly bool) ([]models.Video, error) {
if limit <= 0 {
limit = 5
}
var list []models.Video
q := config.DB.Model(&models.Video{}).
Joins("JOIN album_videos av ON av.video_id = videos.id").
Where("av.album_id = ? AND videos.deleted_at = ?", albumID, 0)
if publishedOnly {
q = q.Where("videos.is_published = ?", 1)
}
err := q.Order("av.sort_order ASC, av.created_at ASC").Limit(limit).Find(&list).Error
return list, err
}
// BuildVideoAlbumResponse 构建专辑响应publishedOnly 控制统计与预览是否仅含已发布视频
func BuildVideoAlbumResponse(item *models.VideoAlbum, publishedOnly bool) models.VideoAlbumResponse {
categoryName := ""
if cat, _ := GetVideoCategoryByID(item.CategoryID); cat != nil {
categoryName = cat.Name
}
latestTs := GetAlbumLatestVideoUpdatedAt(item.ID, publishedOnly)
resp := models.VideoAlbumResponse{
ID: item.ID,
Name: item.Name,
Description: item.Description,
Cover: item.Cover,
CategoryID: item.CategoryID,
CategoryName: categoryName,
IsActive: item.IsActive,
SortOrder: item.SortOrder,
CreatedAt: formatTimestamp(item.CreatedAt),
UpdatedAt: formatTimestamp(item.UpdatedAt),
VideoCount: GetAlbumVideoCount(item.ID, publishedOnly),
}
if latestTs > 0 {
resp.LatestVideoUpdatedAt = formatTimestamp(latestTs)
}
if previews, err := GetAlbumPreviewVideos(item.ID, 5, publishedOnly); err == nil && len(previews) > 0 {
resp.PreviewVideos = BuildVideosResponse(previews)
}
return resp
}
func BuildVideoAlbumsResponse(list []models.VideoAlbum, publishedOnly bool) []models.VideoAlbumResponse {
res := make([]models.VideoAlbumResponse, 0, len(list))
for i := range list {
res = append(res, BuildVideoAlbumResponse(&list[i], publishedOnly))
}
return res
}
// ========== 视频 ==========
func GetVideos(categoryID uint, publishedOnly bool) ([]models.Video, error) {
var list []models.Video
q := config.DB.Model(&models.Video{}).Where("deleted_at = ?", 0)
if categoryID > 0 {
q = q.Where("category_id = ?", categoryID)
}
if publishedOnly {
q = q.Where("is_published = ?", 1)
}
err := q.Order("created_at DESC").Find(&list).Error
return list, err
}
func GetVideoByID(id string) (*models.Video, error) {
var item models.Video
err := config.DB.Model(&models.Video{}).
Where("id = ? AND deleted_at = ?", id, 0).
First(&item).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &item, err
}
func CreateVideo(item *models.Video) error {
return config.DB.Create(item).Error
}
func UpdateVideo(item *models.Video) error {
return config.DB.Model(&models.Video{}).
Where("id = ? AND deleted_at = ?", item.ID, 0).
Updates(map[string]interface{}{
"title": item.Title,
"description": item.Description,
"video_url": item.VideoURL,
"cover": item.Cover,
"poster": item.Poster,
"category_id": item.CategoryID,
"duration": item.Duration,
"is_published": item.IsPublished,
"updated_at": time.Now().Unix(),
}).Error
}
func DeleteVideo(id string) error {
return config.DB.Model(&models.Video{}).
Where("id = ?", id).
Update("deleted_at", time.Now().Unix()).Error
}
func BuildVideoResponse(item *models.Video) models.VideoResponse {
categoryName := ""
if cat, _ := GetVideoCategoryByID(item.CategoryID); cat != nil {
categoryName = cat.Name
}
poster := item.Poster
if poster == "" {
poster = item.Cover
}
return models.VideoResponse{
ID: item.ID,
Title: item.Title,
Description: item.Description,
VideoURL: item.VideoURL,
Cover: item.Cover,
Poster: poster,
CategoryID: item.CategoryID,
CategoryName: categoryName,
Duration: item.Duration,
IsPublished: item.IsPublished,
CreatedAt: formatTimestamp(item.CreatedAt),
UpdatedAt: formatTimestamp(item.UpdatedAt),
}
}
func BuildVideosResponse(list []models.Video) []models.VideoResponse {
res := make([]models.VideoResponse, 0, len(list))
for i := range list {
res = append(res, BuildVideoResponse(&list[i]))
}
return res
}
// ResolveVideoURL 根据 video_id 解析播放地址
func ResolveVideoURL(videoID string) string {
if videoID == "" {
return ""
}
v, err := GetVideoByID(videoID)
if err != nil || v == nil {
log.Printf("ResolveVideoURL: video not found %s", videoID)
return ""
}
return v.VideoURL
}
// ========== 专辑视频关联 ==========
func GetVideosByAlbumID(albumID uint, publishedOnly bool) ([]models.Video, error) {
var list []models.Video
q := config.DB.Model(&models.Video{}).
Joins("JOIN album_videos av ON av.video_id = videos.id").
Where("av.album_id = ? AND videos.deleted_at = ?", albumID, 0)
if publishedOnly {
q = q.Where("videos.is_published = ?", 1)
}
err := q.Order("av.sort_order ASC, av.created_at ASC").Find(&list).Error
return list, err
}
func AddVideoToAlbum(albumID uint, videoID string, sortOrder uint) error {
av := models.AlbumVideo{
AlbumID: albumID,
VideoID: videoID,
SortOrder: sortOrder,
CreatedAt: time.Now().Unix(),
}
return config.DB.Create(&av).Error
}
func RemoveVideoFromAlbum(albumID uint, videoID string) error {
return config.DB.Where("album_id = ? AND video_id = ?", albumID, videoID).
Delete(&models.AlbumVideo{}).Error
}
func SyncAlbumVideos(albumID uint, videoIDs []string) error {
if err := config.DB.Where("album_id = ?", albumID).Delete(&models.AlbumVideo{}).Error; err != nil {
return err
}
for i, vid := range videoIDs {
if vid == "" {
continue
}
if err := AddVideoToAlbum(albumID, vid, uint(i+1)); err != nil {
log.Printf("SyncAlbumVideos error: %v", err)
return err
}
}
return nil
}
// GetAlbumIDsByVideoID 查询视频所属专辑 ID 列表
func GetAlbumIDsByVideoID(videoID string) ([]uint, error) {
var ids []uint
err := config.DB.Model(&models.AlbumVideo{}).
Where("video_id = ?", videoID).
Pluck("album_id", &ids).Error
return ids, err
}
// GetPrimaryActiveAlbumForVideo 取视频关联的第一个启用专辑(按专辑 sort_order
func GetPrimaryActiveAlbumForVideo(videoID string) (*models.VideoAlbum, error) {
var album models.VideoAlbum
err := config.DB.Model(&models.VideoAlbum{}).
Joins("JOIN album_videos av ON av.album_id = video_albums.id").
Where("av.video_id = ? AND video_albums.deleted_at = ? AND video_albums.is_active = ?", videoID, 0, 1).
Order("video_albums.sort_order ASC, video_albums.id ASC").
First(&album).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return &album, err
}
// GetAlbumVideoNeighbors 获取专辑内当前视频的前后邻居(按 sort_order
func GetAlbumVideoNeighbors(albumID uint, videoID string, before, after int, publishedOnly bool) (prev, next []models.Video, err error) {
list, err := GetVideosByAlbumID(albumID, publishedOnly)
if err != nil {
return nil, nil, err
}
idx := -1
for i, v := range list {
if v.ID == videoID {
idx = i
break
}
}
if idx < 0 {
return nil, nil, nil
}
start := idx - before
if start < 0 {
start = 0
}
prev = list[start:idx]
end := idx + after + 1
if end > len(list) {
end = len(list)
}
next = list[idx+1 : end]
return prev, next, nil
}
// BuildVideoDetailResponse 构建视频详情响应(含专辑前后导航)
func BuildVideoDetailResponse(item *models.Video) models.VideoDetailResponse {
resp := models.VideoDetailResponse{
VideoResponse: BuildVideoResponse(item),
}
album, err := GetPrimaryActiveAlbumForVideo(item.ID)
if err != nil || album == nil {
return resp
}
prev, next, err := GetAlbumVideoNeighbors(album.ID, item.ID, 2, 2, true)
if err != nil {
return resp
}
resp.AlbumContext = &models.VideoAlbumNavContext{
AlbumID: album.ID,
AlbumName: album.Name,
PrevVideos: BuildVideosResponse(prev),
NextVideos: BuildVideosResponse(next),
}
return resp
}
// GetAlbumIDsByVideoIDList 供管理端读取视频所属专辑
func GetAlbumIDsByVideoIDList(videoID string) []uint {
ids, err := GetAlbumIDsByVideoID(videoID)
if err != nil {
return []uint{}
}
return ids
}
// BuildAdminVideoResponse 构建管理端视频响应(含 albumIds
func BuildAdminVideoResponse(item *models.Video) models.AdminVideoResponse {
return models.AdminVideoResponse{
VideoResponse: BuildVideoResponse(item),
AlbumIDs: GetAlbumIDsByVideoIDList(item.ID),
}
}
// BuildAdminVideosResponse 批量构建管理端视频响应
func BuildAdminVideosResponse(list []models.Video) []models.AdminVideoResponse {
res := make([]models.AdminVideoResponse, 0, len(list))
for i := range list {
res = append(res, BuildAdminVideoResponse(&list[i]))
}
return res
}
// SyncVideoAlbums 同步视频所属专辑(先删后建)
func SyncVideoAlbums(videoID string, albumIDs []uint) error {
if err := config.DB.Where("video_id = ?", videoID).Delete(&models.AlbumVideo{}).Error; err != nil {
return err
}
for i, albumID := range albumIDs {
if albumID == 0 {
continue
}
if err := AddVideoToAlbum(albumID, videoID, uint(i+1)); err != nil {
log.Printf("SyncVideoAlbums error: %v", err)
return err
}
}
return nil
}