优化页面、修复BUG

This commit is contained in:
李琦
2026-07-06 08:19:47 +08:00
parent e1f852c295
commit 9c99f96e94
10 changed files with 445 additions and 87 deletions

View File

@@ -30,7 +30,7 @@ func GetVideoAlbums(c *gin.Context) {
utils.ServerError(c, err)
return
}
utils.Success(c, repositories.BuildVideoAlbumsResponse(list))
utils.Success(c, repositories.BuildVideoAlbumsResponse(list, true))
}
// GetVideoAlbumByID 获取专辑详情
@@ -49,7 +49,7 @@ func GetVideoAlbumByID(c *gin.Context) {
utils.Error(c, 404, "Album not found")
return
}
utils.Success(c, repositories.BuildVideoAlbumResponse(album))
utils.Success(c, repositories.BuildVideoAlbumResponse(album, true))
}
// GetAlbumVideos 获取专辑内视频
@@ -90,7 +90,7 @@ func GetVideoByID(c *gin.Context) {
utils.Error(c, 404, "Video not found")
return
}
utils.Success(c, repositories.BuildVideoResponse(video))
utils.Success(c, repositories.BuildVideoDetailResponse(video))
}
// ========== 管理 API — 分类 ==========
@@ -158,7 +158,7 @@ func AdminGetVideoAlbums(c *gin.Context) {
utils.ServerError(c, err)
return
}
utils.Success(c, repositories.BuildVideoAlbumsResponse(list))
utils.Success(c, repositories.BuildVideoAlbumsResponse(list, false))
}
func AdminCreateVideoAlbum(c *gin.Context) {
@@ -171,7 +171,7 @@ func AdminCreateVideoAlbum(c *gin.Context) {
utils.ServerError(c, err)
return
}
utils.Success(c, repositories.BuildVideoAlbumResponse(&item))
utils.Success(c, repositories.BuildVideoAlbumResponse(&item, false))
}
func AdminUpdateVideoAlbum(c *gin.Context) {
@@ -199,7 +199,7 @@ func AdminUpdateVideoAlbum(c *gin.Context) {
return
}
}
utils.Success(c, repositories.BuildVideoAlbumResponse(&req.VideoAlbum))
utils.Success(c, repositories.BuildVideoAlbumResponse(&req.VideoAlbum, false))
}
func AdminDeleteVideoAlbum(c *gin.Context) {
@@ -277,38 +277,61 @@ func AdminGetVideos(c *gin.Context) {
utils.ServerError(c, err)
return
}
utils.Success(c, repositories.BuildVideosResponse(list))
utils.Success(c, repositories.BuildAdminVideosResponse(list))
}
func AdminCreateVideo(c *gin.Context) {
var item models.Video
if err := c.ShouldBindJSON(&item); err != nil {
var req struct {
models.Video
AlbumIDs []uint `json:"albumIds"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, err.Error())
return
}
if item.ID == "" {
item.ID = "video_" + strconv.FormatInt(time.Now().UnixNano(), 10)
if req.Video.ID == "" {
req.Video.ID = "video_" + strconv.FormatInt(time.Now().UnixNano(), 10)
}
if err := repositories.CreateVideo(&item); err != nil {
if err := repositories.CreateVideo(&req.Video); err != nil {
utils.ServerError(c, err)
return
}
utils.Success(c, repositories.BuildVideoResponse(&item))
if req.AlbumIDs != nil {
if err := repositories.SyncVideoAlbums(req.Video.ID, req.AlbumIDs); err != nil {
utils.ServerError(c, err)
return
}
}
utils.Success(c, repositories.BuildAdminVideoResponse(&req.Video))
}
func AdminUpdateVideo(c *gin.Context) {
id := c.Param("id")
var item models.Video
if err := c.ShouldBindJSON(&item); err != nil {
var req struct {
models.Video
AlbumIDs []uint `json:"albumIds"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, err.Error())
return
}
item.ID = id
if err := repositories.UpdateVideo(&item); err != nil {
req.Video.ID = id
if err := repositories.UpdateVideo(&req.Video); err != nil {
utils.ServerError(c, err)
return
}
utils.Success(c, repositories.BuildVideoResponse(&item))
if req.AlbumIDs != nil {
if err := repositories.SyncVideoAlbums(id, req.AlbumIDs); err != nil {
utils.ServerError(c, err)
return
}
}
video, _ := repositories.GetVideoByID(id)
if video == nil {
utils.Error(c, 404, "Video not found")
return
}
utils.Success(c, repositories.BuildAdminVideoResponse(video))
}
func AdminDeleteVideo(c *gin.Context) {
@@ -331,5 +354,5 @@ func AdminGetVideoByID(c *gin.Context) {
utils.Error(c, 404, "Video not found")
return
}
utils.Success(c, repositories.BuildVideoResponse(video))
utils.Success(c, repositories.BuildAdminVideoResponse(video))
}

View File

@@ -83,17 +83,39 @@ func (va *VideoAlbum) BeforeUpdate(tx *gorm.DB) error {
// VideoAlbumResponse 视频专辑 API 响应
type VideoAlbumResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Cover string `json:"cover"`
CategoryID uint `json:"categoryId"`
CategoryName string `json:"categoryName,omitempty"`
IsActive int `json:"isActive"`
SortOrder uint `json:"sortOrder"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
VideoCount int64 `json:"videoCount,omitempty"`
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Cover string `json:"cover"`
CategoryID uint `json:"categoryId"`
CategoryName string `json:"categoryName,omitempty"`
IsActive int `json:"isActive"`
SortOrder uint `json:"sortOrder"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
VideoCount int64 `json:"videoCount,omitempty"`
LatestVideoUpdatedAt string `json:"latestVideoUpdatedAt,omitempty"`
PreviewVideos []VideoResponse `json:"previewVideos,omitempty"`
}
// VideoAlbumNavContext 视频详情页专辑内前后导航
type VideoAlbumNavContext struct {
AlbumID uint `json:"albumId"`
AlbumName string `json:"albumName"`
PrevVideos []VideoResponse `json:"prevVideos"`
NextVideos []VideoResponse `json:"nextVideos"`
}
// VideoDetailResponse 视频详情 API 响应(含专辑导航)
type VideoDetailResponse struct {
VideoResponse
AlbumContext *VideoAlbumNavContext `json:"albumContext,omitempty"`
}
// AdminVideoResponse 管理端视频响应(含所属专辑 ID
type AdminVideoResponse struct {
VideoResponse
AlbumIDs []uint `json:"albumIds,omitempty"`
}
// Video 视频模型

View File

@@ -139,21 +139,57 @@ func DeleteVideoAlbum(id uint) error {
Update("deleted_at", time.Now().Unix()).Error
}
func GetAlbumVideoCount(albumID uint) int64 {
// GetAlbumVideoCount 统计专辑内视频数量publishedOnly 为 true 时仅统计已发布视频
func GetAlbumVideoCount(albumID uint, publishedOnly bool) int64 {
var count int64
config.DB.Model(&models.AlbumVideo{}).
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 = ? AND v.is_published = ?", albumID, 0, 1).
Count(&count)
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
}
func BuildVideoAlbumResponse(item *models.VideoAlbum) models.VideoAlbumResponse {
// 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
}
return models.VideoAlbumResponse{
latestTs := GetAlbumLatestVideoUpdatedAt(item.ID, publishedOnly)
resp := models.VideoAlbumResponse{
ID: item.ID,
Name: item.Name,
Description: item.Description,
@@ -164,14 +200,21 @@ func BuildVideoAlbumResponse(item *models.VideoAlbum) models.VideoAlbumResponse
SortOrder: item.SortOrder,
CreatedAt: formatTimestamp(item.CreatedAt),
UpdatedAt: formatTimestamp(item.UpdatedAt),
VideoCount: GetAlbumVideoCount(item.ID),
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) []models.VideoAlbumResponse {
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]))
res = append(res, BuildVideoAlbumResponse(&list[i], publishedOnly))
}
return res
}
@@ -318,3 +361,120 @@ func SyncAlbumVideos(albumID uint, videoIDs []string) error {
}
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
}