481 lines
14 KiB
Go
481 lines
14 KiB
Go
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
|
||
}
|