优化页面、修复BUG
This commit is contained in:
335
server/handlers/video.go
Normal file
335
server/handlers/video.go
Normal file
@@ -0,0 +1,335 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/niangaodev/art-code/models"
|
||||
"github.com/niangaodev/art-code/repositories"
|
||||
"github.com/niangaodev/art-code/utils"
|
||||
)
|
||||
|
||||
// ========== 公开 API ==========
|
||||
|
||||
// GetVideoCategories 获取视频分类列表
|
||||
func GetVideoCategories(c *gin.Context) {
|
||||
list, err := repositories.GetVideoCategories()
|
||||
if err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideoCategoriesResponse(list))
|
||||
}
|
||||
|
||||
// GetVideoAlbums 获取视频专辑列表
|
||||
func GetVideoAlbums(c *gin.Context) {
|
||||
categoryID, _ := strconv.ParseUint(c.Query("categoryId"), 10, 32)
|
||||
list, err := repositories.GetActiveVideoAlbums(uint(categoryID))
|
||||
if err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideoAlbumsResponse(list))
|
||||
}
|
||||
|
||||
// GetVideoAlbumByID 获取专辑详情
|
||||
func GetVideoAlbumByID(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, "Invalid album ID")
|
||||
return
|
||||
}
|
||||
album, err := repositories.GetVideoAlbumByID(uint(id))
|
||||
if err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
if album == nil || album.IsActive != 1 {
|
||||
utils.Error(c, 404, "Album not found")
|
||||
return
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideoAlbumResponse(album))
|
||||
}
|
||||
|
||||
// GetAlbumVideos 获取专辑内视频
|
||||
func GetAlbumVideos(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, "Invalid album ID")
|
||||
return
|
||||
}
|
||||
list, err := repositories.GetVideosByAlbumID(uint(id), true)
|
||||
if err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideosResponse(list))
|
||||
}
|
||||
|
||||
// GetVideos 获取视频列表
|
||||
func GetVideos(c *gin.Context) {
|
||||
categoryID, _ := strconv.ParseUint(c.Query("categoryId"), 10, 32)
|
||||
list, err := repositories.GetVideos(uint(categoryID), true)
|
||||
if err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideosResponse(list))
|
||||
}
|
||||
|
||||
// GetVideoByID 获取视频详情
|
||||
func GetVideoByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
video, err := repositories.GetVideoByID(id)
|
||||
if err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
if video == nil || video.IsPublished != 1 {
|
||||
utils.Error(c, 404, "Video not found")
|
||||
return
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideoResponse(video))
|
||||
}
|
||||
|
||||
// ========== 管理 API — 分类 ==========
|
||||
|
||||
func AdminGetVideoCategories(c *gin.Context) {
|
||||
list, err := repositories.GetVideoCategories()
|
||||
if err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideoCategoriesResponse(list))
|
||||
}
|
||||
|
||||
func AdminCreateVideoCategory(c *gin.Context) {
|
||||
var item models.VideoCategory
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if err := repositories.CreateVideoCategory(&item); err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideoCategoryResponse(&item))
|
||||
}
|
||||
|
||||
func AdminUpdateVideoCategory(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, "Invalid category ID")
|
||||
return
|
||||
}
|
||||
var item models.VideoCategory
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = uint(id)
|
||||
if err := repositories.UpdateVideoCategory(&item); err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideoCategoryResponse(&item))
|
||||
}
|
||||
|
||||
func AdminDeleteVideoCategory(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, "Invalid category ID")
|
||||
return
|
||||
}
|
||||
if err := repositories.DeleteVideoCategory(uint(id)); err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMsg(c, "Category deleted", nil)
|
||||
}
|
||||
|
||||
// ========== 管理 API — 专辑 ==========
|
||||
|
||||
func AdminGetVideoAlbums(c *gin.Context) {
|
||||
categoryID, _ := strconv.ParseUint(c.Query("categoryId"), 10, 32)
|
||||
list, err := repositories.GetVideoAlbums(uint(categoryID))
|
||||
if err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideoAlbumsResponse(list))
|
||||
}
|
||||
|
||||
func AdminCreateVideoAlbum(c *gin.Context) {
|
||||
var item models.VideoAlbum
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if err := repositories.CreateVideoAlbum(&item); err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideoAlbumResponse(&item))
|
||||
}
|
||||
|
||||
func AdminUpdateVideoAlbum(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, "Invalid album ID")
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
models.VideoAlbum
|
||||
VideoIDs []string `json:"videoIds"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
req.VideoAlbum.ID = uint(id)
|
||||
if err := repositories.UpdateVideoAlbum(&req.VideoAlbum); err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
if req.VideoIDs != nil {
|
||||
if err := repositories.SyncAlbumVideos(uint(id), req.VideoIDs); err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideoAlbumResponse(&req.VideoAlbum))
|
||||
}
|
||||
|
||||
func AdminDeleteVideoAlbum(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, "Invalid album ID")
|
||||
return
|
||||
}
|
||||
if err := repositories.DeleteVideoAlbum(uint(id)); err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMsg(c, "Album deleted", nil)
|
||||
}
|
||||
|
||||
func AdminAddVideoToAlbum(c *gin.Context) {
|
||||
albumID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, "Invalid album ID")
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
VideoID string `json:"videoId"`
|
||||
SortOrder uint `json:"sortOrder"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if err := repositories.AddVideoToAlbum(uint(albumID), req.VideoID, req.SortOrder); err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMsg(c, "Video added to album", nil)
|
||||
}
|
||||
|
||||
func AdminRemoveVideoFromAlbum(c *gin.Context) {
|
||||
albumID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, "Invalid album ID")
|
||||
return
|
||||
}
|
||||
videoID := c.Param("videoId")
|
||||
if err := repositories.RemoveVideoFromAlbum(uint(albumID), videoID); err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMsg(c, "Video removed from album", nil)
|
||||
}
|
||||
|
||||
func AdminGetAlbumVideoIDs(c *gin.Context) {
|
||||
albumID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
utils.Error(c, 400, "Invalid album ID")
|
||||
return
|
||||
}
|
||||
videos, err := repositories.GetVideosByAlbumID(uint(albumID), false)
|
||||
if err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
ids := make([]string, 0, len(videos))
|
||||
for _, v := range videos {
|
||||
ids = append(ids, v.ID)
|
||||
}
|
||||
utils.Success(c, ids)
|
||||
}
|
||||
|
||||
// ========== 管理 API — 视频 ==========
|
||||
|
||||
func AdminGetVideos(c *gin.Context) {
|
||||
categoryID, _ := strconv.ParseUint(c.Query("categoryId"), 10, 32)
|
||||
list, err := repositories.GetVideos(uint(categoryID), false)
|
||||
if err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideosResponse(list))
|
||||
}
|
||||
|
||||
func AdminCreateVideo(c *gin.Context) {
|
||||
var item models.Video
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if item.ID == "" {
|
||||
item.ID = "video_" + strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
}
|
||||
if err := repositories.CreateVideo(&item); err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideoResponse(&item))
|
||||
}
|
||||
|
||||
func AdminUpdateVideo(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var item models.Video
|
||||
if err := c.ShouldBindJSON(&item); err != nil {
|
||||
utils.Error(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
item.ID = id
|
||||
if err := repositories.UpdateVideo(&item); err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideoResponse(&item))
|
||||
}
|
||||
|
||||
func AdminDeleteVideo(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := repositories.DeleteVideo(id); err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
utils.SuccessWithMsg(c, "Video deleted", nil)
|
||||
}
|
||||
|
||||
func AdminGetVideoByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
video, err := repositories.GetVideoByID(id)
|
||||
if err != nil {
|
||||
utils.ServerError(c, err)
|
||||
return
|
||||
}
|
||||
if video == nil {
|
||||
utils.Error(c, 404, "Video not found")
|
||||
return
|
||||
}
|
||||
utils.Success(c, repositories.BuildVideoResponse(video))
|
||||
}
|
||||
@@ -27,6 +27,7 @@ func main() {
|
||||
repositories.MigratePostUserID()
|
||||
repositories.MigrateUserAvatar()
|
||||
repositories.MigrateUserProfileFields()
|
||||
repositories.MigrateVideoModule()
|
||||
|
||||
// 初始化ip2region (如果文件不存在,将降级为普通IP记录)
|
||||
// 函数会自动从环境变量或可执行文件目录查找 ip2region.xdb
|
||||
@@ -59,6 +60,14 @@ func main() {
|
||||
api.GET("/works", handlers.GetWorks)
|
||||
api.GET("/works/:id", handlers.GetWorkByID)
|
||||
|
||||
// 视频路由
|
||||
api.GET("/video-categories", handlers.GetVideoCategories)
|
||||
api.GET("/video-albums", handlers.GetVideoAlbums)
|
||||
api.GET("/video-albums/:id", handlers.GetVideoAlbumByID)
|
||||
api.GET("/video-albums/:id/videos", handlers.GetAlbumVideos)
|
||||
api.GET("/videos", handlers.GetVideos)
|
||||
api.GET("/videos/:id", handlers.GetVideoByID)
|
||||
|
||||
// 博客路由
|
||||
api.GET("/posts", handlers.GetPosts)
|
||||
api.GET("/posts/:id", handlers.GetPost)
|
||||
@@ -146,6 +155,26 @@ func main() {
|
||||
authAdmin.PUT("/works/:id", middleware.PermissionMiddleware("works", "update"), handlers.AdminUpdateWork)
|
||||
authAdmin.DELETE("/works/:id", middleware.PermissionMiddleware("works", "delete"), handlers.AdminDeleteWork)
|
||||
|
||||
// 视频管理
|
||||
authAdmin.GET("/video-categories", middleware.PermissionMiddleware("videos", "read"), handlers.AdminGetVideoCategories)
|
||||
authAdmin.POST("/video-categories", middleware.PermissionMiddleware("videos", "create"), handlers.AdminCreateVideoCategory)
|
||||
authAdmin.PUT("/video-categories/:id", middleware.PermissionMiddleware("videos", "update"), handlers.AdminUpdateVideoCategory)
|
||||
authAdmin.DELETE("/video-categories/:id", middleware.PermissionMiddleware("videos", "delete"), handlers.AdminDeleteVideoCategory)
|
||||
|
||||
authAdmin.GET("/video-albums", middleware.PermissionMiddleware("videos", "read"), handlers.AdminGetVideoAlbums)
|
||||
authAdmin.POST("/video-albums", middleware.PermissionMiddleware("videos", "create"), handlers.AdminCreateVideoAlbum)
|
||||
authAdmin.PUT("/video-albums/:id", middleware.PermissionMiddleware("videos", "update"), handlers.AdminUpdateVideoAlbum)
|
||||
authAdmin.DELETE("/video-albums/:id", middleware.PermissionMiddleware("videos", "delete"), handlers.AdminDeleteVideoAlbum)
|
||||
authAdmin.GET("/video-albums/:id/video-ids", middleware.PermissionMiddleware("videos", "read"), handlers.AdminGetAlbumVideoIDs)
|
||||
authAdmin.POST("/video-albums/:id/videos", middleware.PermissionMiddleware("videos", "update"), handlers.AdminAddVideoToAlbum)
|
||||
authAdmin.DELETE("/video-albums/:id/videos/:videoId", middleware.PermissionMiddleware("videos", "update"), handlers.AdminRemoveVideoFromAlbum)
|
||||
|
||||
authAdmin.GET("/videos", middleware.PermissionMiddleware("videos", "read"), handlers.AdminGetVideos)
|
||||
authAdmin.GET("/videos/:id", middleware.PermissionMiddleware("videos", "read"), handlers.AdminGetVideoByID)
|
||||
authAdmin.POST("/videos", middleware.PermissionMiddleware("videos", "create"), handlers.AdminCreateVideo)
|
||||
authAdmin.PUT("/videos/:id", middleware.PermissionMiddleware("videos", "update"), handlers.AdminUpdateVideo)
|
||||
authAdmin.DELETE("/videos/:id", middleware.PermissionMiddleware("videos", "delete"), handlers.AdminDeleteVideo)
|
||||
|
||||
// 代码片段管理
|
||||
authAdmin.GET("/snippets", middleware.PermissionMiddleware("snippets", "read"), handlers.AdminGetSnippets)
|
||||
authAdmin.POST("/snippets", middleware.PermissionMiddleware("snippets", "create"), handlers.AdminCreateSnippet)
|
||||
|
||||
164
server/models/video.go
Normal file
164
server/models/video.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// VideoCategory 视频分类模型
|
||||
type VideoCategory struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;column:id"`
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Slug string `json:"slug" gorm:"column:slug;uniqueIndex"`
|
||||
Description string `json:"description" gorm:"column:description;type:text"`
|
||||
SortOrder uint `json:"sortOrder" gorm:"column:sort_order;default:0"`
|
||||
CreatedAt int64 `json:"createdAt" gorm:"column:created_at"`
|
||||
UpdatedAt int64 `json:"updatedAt" gorm:"column:updated_at"`
|
||||
DeletedAt int64 `json:"deletedAt" gorm:"column:deleted_at;default:0"`
|
||||
}
|
||||
|
||||
func (VideoCategory) TableName() string { return "video_categories" }
|
||||
|
||||
func (vc *VideoCategory) BeforeCreate(tx *gorm.DB) error {
|
||||
now := time.Now().Unix()
|
||||
if vc.CreatedAt == 0 {
|
||||
vc.CreatedAt = now
|
||||
}
|
||||
if vc.UpdatedAt == 0 {
|
||||
vc.UpdatedAt = now
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vc *VideoCategory) BeforeUpdate(tx *gorm.DB) error {
|
||||
vc.UpdatedAt = time.Now().Unix()
|
||||
return nil
|
||||
}
|
||||
|
||||
// VideoCategoryResponse 视频分类 API 响应
|
||||
type VideoCategoryResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
SortOrder uint `json:"sortOrder"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
VideoCount int64 `json:"videoCount,omitempty"`
|
||||
AlbumCount int64 `json:"albumCount,omitempty"`
|
||||
}
|
||||
|
||||
// VideoAlbum 视频专辑模型
|
||||
type VideoAlbum struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;column:id"`
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Description string `json:"description" gorm:"column:description;type:text"`
|
||||
Cover string `json:"cover" gorm:"column:cover"`
|
||||
CategoryID uint `json:"categoryId" gorm:"column:category_id;index"`
|
||||
IsActive int `json:"isActive" gorm:"column:is_active;default:1"`
|
||||
SortOrder uint `json:"sortOrder" gorm:"column:sort_order;default:0"`
|
||||
CreatedAt int64 `json:"createdAt" gorm:"column:created_at"`
|
||||
UpdatedAt int64 `json:"updatedAt" gorm:"column:updated_at"`
|
||||
DeletedAt int64 `json:"deletedAt" gorm:"column:deleted_at;default:0"`
|
||||
}
|
||||
|
||||
func (VideoAlbum) TableName() string { return "video_albums" }
|
||||
|
||||
func (va *VideoAlbum) BeforeCreate(tx *gorm.DB) error {
|
||||
now := time.Now().Unix()
|
||||
if va.CreatedAt == 0 {
|
||||
va.CreatedAt = now
|
||||
}
|
||||
if va.UpdatedAt == 0 {
|
||||
va.UpdatedAt = now
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (va *VideoAlbum) BeforeUpdate(tx *gorm.DB) error {
|
||||
va.UpdatedAt = time.Now().Unix()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// Video 视频模型
|
||||
type Video struct {
|
||||
ID string `json:"id" gorm:"primaryKey;column:id"`
|
||||
Title string `json:"title" gorm:"column:title"`
|
||||
Description string `json:"description" gorm:"column:description;type:text"`
|
||||
VideoURL string `json:"videoUrl" gorm:"column:video_url"`
|
||||
Cover string `json:"cover" gorm:"column:cover"`
|
||||
Poster string `json:"poster" gorm:"column:poster"`
|
||||
CategoryID uint `json:"categoryId" gorm:"column:category_id;index"`
|
||||
Duration int `json:"duration" gorm:"column:duration;default:0"`
|
||||
IsPublished int `json:"isPublished" gorm:"column:is_published;default:1"`
|
||||
CreatedAt int64 `json:"createdAt" gorm:"column:created_at"`
|
||||
UpdatedAt int64 `json:"updatedAt" gorm:"column:updated_at"`
|
||||
DeletedAt int64 `json:"deletedAt" gorm:"column:deleted_at;default:0"`
|
||||
}
|
||||
|
||||
func (Video) TableName() string { return "videos" }
|
||||
|
||||
func (v *Video) BeforeCreate(tx *gorm.DB) error {
|
||||
now := time.Now().Unix()
|
||||
if v.CreatedAt == 0 {
|
||||
v.CreatedAt = now
|
||||
}
|
||||
if v.UpdatedAt == 0 {
|
||||
v.UpdatedAt = now
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Video) BeforeUpdate(tx *gorm.DB) error {
|
||||
v.UpdatedAt = time.Now().Unix()
|
||||
return nil
|
||||
}
|
||||
|
||||
// VideoResponse 视频 API 响应
|
||||
type VideoResponse struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
VideoURL string `json:"videoUrl"`
|
||||
Cover string `json:"cover"`
|
||||
Poster string `json:"poster"`
|
||||
CategoryID uint `json:"categoryId"`
|
||||
CategoryName string `json:"categoryName,omitempty"`
|
||||
Duration int `json:"duration"`
|
||||
IsPublished int `json:"isPublished"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// AlbumVideo 专辑视频关联
|
||||
type AlbumVideo struct {
|
||||
AlbumID uint `json:"albumId" gorm:"primaryKey;column:album_id"`
|
||||
VideoID string `json:"videoId" gorm:"primaryKey;column:video_id"`
|
||||
SortOrder uint `json:"sortOrder" gorm:"column:sort_order;default:0"`
|
||||
CreatedAt int64 `json:"createdAt" gorm:"column:created_at"`
|
||||
}
|
||||
|
||||
func (AlbumVideo) TableName() string { return "album_videos" }
|
||||
|
||||
func (av *AlbumVideo) BeforeCreate(tx *gorm.DB) error {
|
||||
if av.CreatedAt == 0 {
|
||||
av.CreatedAt = time.Now().Unix()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -13,6 +13,8 @@ type Work struct {
|
||||
Category string `json:"category" gorm:"column:category"`
|
||||
Year string `json:"year" gorm:"column:year"`
|
||||
HeroImg string `json:"heroImg" gorm:"column:hero_img"`
|
||||
HeroVideo string `json:"heroVideo" gorm:"column:hero_video"`
|
||||
VideoID string `json:"videoId" gorm:"column:video_id"`
|
||||
Description string `json:"desc" gorm:"column:description;type:text"`
|
||||
Links string `json:"links" gorm:"column:links;type:text"` // JSON格式存储链接
|
||||
IsFeatured int `json:"isFeatured" gorm:"column:is_featured;default:0"`
|
||||
@@ -107,6 +109,9 @@ type WorkResponse struct {
|
||||
Category string `json:"category"`
|
||||
Year string `json:"year"`
|
||||
HeroImg string `json:"heroImg"`
|
||||
HeroVideo string `json:"heroVideo,omitempty"`
|
||||
VideoID string `json:"videoId,omitempty"`
|
||||
VideoURL string `json:"videoUrl,omitempty"`
|
||||
Desc string `json:"desc"`
|
||||
TechStack []map[string]interface{} `json:"techStack"`
|
||||
Gallery []string `json:"gallery"`
|
||||
|
||||
@@ -540,6 +540,8 @@ CREATE TABLE `works` (
|
||||
`category` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '作品分类',
|
||||
`year` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '创作年份',
|
||||
`hero_img` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '作品主图URL',
|
||||
`hero_video` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '演示视频URL',
|
||||
`video_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联视频库ID',
|
||||
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '作品详细描述',
|
||||
`links` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '作品链接(JSON格式)',
|
||||
`is_featured` tinyint(1) NULL DEFAULT 0 COMMENT '是否为精选作品(0:否,1:是)',
|
||||
@@ -566,4 +568,74 @@ CREATE TABLE `post_snippets` (
|
||||
INDEX `idx_sort_order`(`sort_order` ASC) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文章代码片段关联表' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for video_categories
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `video_categories`;
|
||||
CREATE TABLE `video_categories` (
|
||||
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) NOT NULL,
|
||||
`slug` varchar(100) NOT NULL,
|
||||
`description` text NULL,
|
||||
`sort_order` int UNSIGNED NOT NULL DEFAULT 0,
|
||||
`deleted_at` bigint NOT NULL DEFAULT 0,
|
||||
`created_at` bigint NOT NULL DEFAULT 0,
|
||||
`updated_at` bigint NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE INDEX `idx_slug`(`slug`)
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '视频分类表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for video_albums
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `video_albums`;
|
||||
CREATE TABLE `video_albums` (
|
||||
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(200) NOT NULL,
|
||||
`description` text NULL,
|
||||
`cover` varchar(500) NULL,
|
||||
`category_id` int UNSIGNED NOT NULL DEFAULT 0,
|
||||
`is_active` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`sort_order` int UNSIGNED NOT NULL DEFAULT 0,
|
||||
`deleted_at` bigint NOT NULL DEFAULT 0,
|
||||
`created_at` bigint NOT NULL DEFAULT 0,
|
||||
`updated_at` bigint NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_category_id`(`category_id`)
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '视频专辑表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for videos
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `videos`;
|
||||
CREATE TABLE `videos` (
|
||||
`id` varchar(50) NOT NULL,
|
||||
`title` varchar(200) NOT NULL,
|
||||
`description` text NULL,
|
||||
`video_url` varchar(500) NOT NULL,
|
||||
`cover` varchar(500) NULL,
|
||||
`poster` varchar(500) NULL,
|
||||
`category_id` int UNSIGNED NOT NULL DEFAULT 0,
|
||||
`duration` int NOT NULL DEFAULT 0,
|
||||
`is_published` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`deleted_at` bigint NOT NULL DEFAULT 0,
|
||||
`created_at` bigint NOT NULL DEFAULT 0,
|
||||
`updated_at` bigint NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_category_id`(`category_id`)
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '视频表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for album_videos
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `album_videos`;
|
||||
CREATE TABLE `album_videos` (
|
||||
`album_id` int UNSIGNED NOT NULL,
|
||||
`video_id` varchar(50) NOT NULL,
|
||||
`sort_order` int UNSIGNED NOT NULL DEFAULT 0,
|
||||
`created_at` bigint NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`album_id`, `video_id`),
|
||||
INDEX `idx_video_id`(`video_id`)
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '专辑视频关联表';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
@@ -3,6 +3,7 @@ package repositories
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/niangaodev/art-code/config"
|
||||
)
|
||||
@@ -195,3 +196,100 @@ func MigrateUserProfileFields() {
|
||||
execSQL("ALTER TABLE `users` ADD COLUMN `wechat_qrcode` VARCHAR(500) NULL DEFAULT NULL COMMENT '微信二维码图片URL' AFTER `wechat`")
|
||||
}
|
||||
}
|
||||
|
||||
// MigrateVideoModule 创建视频模块表及作品视频字段
|
||||
func MigrateVideoModule() {
|
||||
log.Printf("Migrating video module...")
|
||||
|
||||
if !tableExists("video_categories") {
|
||||
execSQL(`CREATE TABLE video_categories (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
slug VARCHAR(100) NOT NULL,
|
||||
description TEXT NULL,
|
||||
sort_order INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
deleted_at BIGINT NOT NULL DEFAULT 0,
|
||||
created_at BIGINT NOT NULL DEFAULT 0,
|
||||
updated_at BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX idx_slug (slug)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='视频分类表'`)
|
||||
}
|
||||
|
||||
if !tableExists("video_albums") {
|
||||
execSQL(`CREATE TABLE video_albums (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
description TEXT NULL,
|
||||
cover VARCHAR(500) NULL,
|
||||
category_id INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
sort_order INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
deleted_at BIGINT NOT NULL DEFAULT 0,
|
||||
created_at BIGINT NOT NULL DEFAULT 0,
|
||||
updated_at BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
INDEX idx_category_id (category_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='视频专辑表'`)
|
||||
}
|
||||
|
||||
if !tableExists("videos") {
|
||||
execSQL(`CREATE TABLE videos (
|
||||
id VARCHAR(50) NOT NULL,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
description TEXT NULL,
|
||||
video_url VARCHAR(500) NOT NULL,
|
||||
cover VARCHAR(500) NULL,
|
||||
poster VARCHAR(500) NULL,
|
||||
category_id INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
duration INT NOT NULL DEFAULT 0,
|
||||
is_published TINYINT(1) NOT NULL DEFAULT 1,
|
||||
deleted_at BIGINT NOT NULL DEFAULT 0,
|
||||
created_at BIGINT NOT NULL DEFAULT 0,
|
||||
updated_at BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
INDEX idx_category_id (category_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='视频表'`)
|
||||
}
|
||||
|
||||
if !tableExists("album_videos") {
|
||||
execSQL(`CREATE TABLE album_videos (
|
||||
album_id INT UNSIGNED NOT NULL,
|
||||
video_id VARCHAR(50) NOT NULL,
|
||||
sort_order INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_at BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (album_id, video_id),
|
||||
INDEX idx_video_id (video_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='专辑视频关联表'`)
|
||||
}
|
||||
|
||||
if !columnExists("works", "hero_video") {
|
||||
execSQL("ALTER TABLE `works` ADD COLUMN `hero_video` VARCHAR(500) NULL DEFAULT NULL COMMENT '演示视频URL' AFTER `hero_img`")
|
||||
}
|
||||
if !columnExists("works", "video_id") {
|
||||
execSQL("ALTER TABLE `works` ADD COLUMN `video_id` VARCHAR(50) NULL DEFAULT NULL COMMENT '关联视频库ID' AFTER `hero_video`")
|
||||
}
|
||||
|
||||
// 插入 videos 权限(若不存在)
|
||||
now := time.Now().Unix()
|
||||
actions := []string{"read", "create", "update", "delete"}
|
||||
for _, action := range actions {
|
||||
var count int64
|
||||
config.DB.Raw("SELECT COUNT(*) FROM permissions WHERE resource = ? AND action = ? AND deleted_at = 0", "videos", action).Scan(&count)
|
||||
if count == 0 {
|
||||
execSQL(fmt.Sprintf("INSERT INTO permissions (name, resource, action, deleted_at, created_at, updated_at) VALUES ('videos:%s', 'videos', '%s', 0, %d, %d)", action, action, now, now))
|
||||
}
|
||||
}
|
||||
|
||||
// 为 role_id=1 的管理员角色授予 videos 权限
|
||||
type permRow struct{ ID uint }
|
||||
var rows []permRow
|
||||
config.DB.Raw("SELECT id FROM permissions WHERE resource = 'videos' AND deleted_at = 0").Scan(&rows)
|
||||
for _, row := range rows {
|
||||
var count int64
|
||||
config.DB.Raw("SELECT COUNT(*) FROM role_permissions WHERE role_id = 1 AND permission_id = ?", row.ID).Scan(&count)
|
||||
if count == 0 {
|
||||
execSQL(fmt.Sprintf("INSERT INTO role_permissions (role_id, permission_id) VALUES (1, %d)", row.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
320
server/repositories/video_repository.go
Normal file
320
server/repositories/video_repository.go
Normal file
@@ -0,0 +1,320 @@
|
||||
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
|
||||
}
|
||||
|
||||
func GetAlbumVideoCount(albumID uint) int64 {
|
||||
var count int64
|
||||
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)
|
||||
return count
|
||||
}
|
||||
|
||||
func BuildVideoAlbumResponse(item *models.VideoAlbum) models.VideoAlbumResponse {
|
||||
categoryName := ""
|
||||
if cat, _ := GetVideoCategoryByID(item.CategoryID); cat != nil {
|
||||
categoryName = cat.Name
|
||||
}
|
||||
return 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),
|
||||
}
|
||||
}
|
||||
|
||||
func BuildVideoAlbumsResponse(list []models.VideoAlbum) []models.VideoAlbumResponse {
|
||||
res := make([]models.VideoAlbumResponse, 0, len(list))
|
||||
for i := range list {
|
||||
res = append(res, BuildVideoAlbumResponse(&list[i]))
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -120,12 +120,24 @@ func BuildWorkResponse(work *models.Work) (*models.WorkResponse, error) {
|
||||
nextWorkID = ""
|
||||
}
|
||||
|
||||
// 解析视频播放地址:优先 video_id,否则 hero_video
|
||||
videoURL := ""
|
||||
if work.VideoID != "" {
|
||||
videoURL = ResolveVideoURL(work.VideoID)
|
||||
}
|
||||
if videoURL == "" {
|
||||
videoURL = work.HeroVideo
|
||||
}
|
||||
|
||||
return &models.WorkResponse{
|
||||
ID: work.ID,
|
||||
Title: work.Title,
|
||||
Category: work.Category,
|
||||
Year: work.Year,
|
||||
HeroImg: work.HeroImg,
|
||||
HeroVideo: work.HeroVideo,
|
||||
VideoID: work.VideoID,
|
||||
VideoURL: videoURL,
|
||||
Desc: work.Description,
|
||||
TechStack: techStackResponse,
|
||||
Gallery: galleryImages,
|
||||
@@ -255,6 +267,8 @@ func UpdateWork(work *models.Work) error {
|
||||
"category": work.Category,
|
||||
"year": work.Year,
|
||||
"hero_img": work.HeroImg,
|
||||
"hero_video": work.HeroVideo,
|
||||
"video_id": work.VideoID,
|
||||
"description": work.Description,
|
||||
"is_featured": work.IsFeatured,
|
||||
"updated_at": time.Now().Unix(),
|
||||
|
||||
127
server/scripts/migrate_video_module.sql
Normal file
127
server/scripts/migrate_video_module.sql
Normal file
@@ -0,0 +1,127 @@
|
||||
-- ============================================================
|
||||
-- 视频模块 + 作品演示视频 数据库迁移脚本
|
||||
-- 文件:server/scripts/migrate_video_module.sql
|
||||
-- 说明:可重复执行(幂等),适用于已有 nl_blog 数据库的增量升级
|
||||
-- 用法:mysql -u root -p your_database < scripts/migrate_video_module.sql
|
||||
-- ============================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ----------------------------
|
||||
-- 1. 视频分类表
|
||||
-- ----------------------------
|
||||
CREATE TABLE IF NOT EXISTS `video_categories` (
|
||||
`id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '分类ID',
|
||||
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '分类名称',
|
||||
`slug` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'URL标识',
|
||||
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '分类描述',
|
||||
`sort_order` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '排序(越小越靠前)',
|
||||
`deleted_at` bigint NOT NULL DEFAULT 0 COMMENT '软删除时间戳',
|
||||
`created_at` bigint NOT NULL DEFAULT 0 COMMENT '创建时间(Unix秒)',
|
||||
`updated_at` bigint NOT NULL DEFAULT 0 COMMENT '更新时间(Unix秒)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `idx_slug`(`slug` ASC) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '视频分类表' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- 2. 视频专辑表
|
||||
-- ----------------------------
|
||||
CREATE TABLE IF NOT EXISTS `video_albums` (
|
||||
`id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '专辑ID',
|
||||
`name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '专辑名称',
|
||||
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '专辑描述',
|
||||
`cover` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '专辑封面URL',
|
||||
`category_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '所属视频分类ID',
|
||||
`is_active` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用(0否 1是)',
|
||||
`sort_order` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '排序(越小越靠前)',
|
||||
`deleted_at` bigint NOT NULL DEFAULT 0 COMMENT '软删除时间戳',
|
||||
`created_at` bigint NOT NULL DEFAULT 0 COMMENT '创建时间(Unix秒)',
|
||||
`updated_at` bigint NOT NULL DEFAULT 0 COMMENT '更新时间(Unix秒)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_category_id`(`category_id` ASC) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '视频专辑表' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- 3. 视频表
|
||||
-- ----------------------------
|
||||
CREATE TABLE IF NOT EXISTS `videos` (
|
||||
`id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '视频唯一标识',
|
||||
`title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '视频标题',
|
||||
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '视频描述',
|
||||
`video_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '视频文件URL',
|
||||
`cover` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '封面图URL',
|
||||
`poster` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '播放器封面URL',
|
||||
`category_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '所属视频分类ID',
|
||||
`duration` int NOT NULL DEFAULT 0 COMMENT '时长(秒)',
|
||||
`is_published` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否发布(0草稿 1已发布)',
|
||||
`deleted_at` bigint NOT NULL DEFAULT 0 COMMENT '软删除时间戳',
|
||||
`created_at` bigint NOT NULL DEFAULT 0 COMMENT '创建时间(Unix秒)',
|
||||
`updated_at` bigint NOT NULL DEFAULT 0 COMMENT '更新时间(Unix秒)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_category_id`(`category_id` ASC) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '视频表' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- 4. 专辑-视频关联表
|
||||
-- ----------------------------
|
||||
CREATE TABLE IF NOT EXISTS `album_videos` (
|
||||
`album_id` int UNSIGNED NOT NULL COMMENT '专辑ID',
|
||||
`video_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '视频ID',
|
||||
`sort_order` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '专辑内排序',
|
||||
`created_at` bigint NOT NULL DEFAULT 0 COMMENT '创建时间(Unix秒)',
|
||||
PRIMARY KEY (`album_id`, `video_id`) USING BTREE,
|
||||
INDEX `idx_video_id`(`video_id` ASC) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '专辑视频关联表' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- 5. 作品表新增演示视频字段(幂等添加)
|
||||
-- ----------------------------
|
||||
|
||||
-- 5.1 hero_video:独立上传的视频 URL
|
||||
SET @col_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'works' AND COLUMN_NAME = 'hero_video'
|
||||
);
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE `works` ADD COLUMN `hero_video` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT ''演示视频URL'' AFTER `hero_img`',
|
||||
'SELECT ''works.hero_video already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 5.2 video_id:关联 videos 表 ID(从视频库选择时使用)
|
||||
SET @col_exists = (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'works' AND COLUMN_NAME = 'video_id'
|
||||
);
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE `works` ADD COLUMN `video_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT ''关联视频库ID'' AFTER `hero_video`',
|
||||
'SELECT ''works.video_id already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ----------------------------
|
||||
-- 6. 视频模块后台权限(videos:read/create/update/delete)
|
||||
-- ----------------------------
|
||||
INSERT IGNORE INTO `permissions` (`name`, `resource`, `action`, `deleted_at`, `created_at`, `updated_at`) VALUES
|
||||
('videos:read', 'videos', 'read', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
|
||||
('videos:create', 'videos', 'create', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
|
||||
('videos:update', 'videos', 'update', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
|
||||
('videos:delete', 'videos', 'delete', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
|
||||
|
||||
-- 为管理员角色(role_id = 1)授予 videos 权限
|
||||
INSERT IGNORE INTO `role_permissions` (`role_id`, `permission_id`)
|
||||
SELECT 1, p.id FROM `permissions` p
|
||||
WHERE p.resource = 'videos' AND p.deleted_at = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `role_permissions` rp
|
||||
WHERE rp.role_id = 1 AND rp.permission_id = p.id
|
||||
);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
SELECT 'migrate_video_module.sql executed successfully' AS result;
|
||||
Reference in New Issue
Block a user