fix(P2): rand.Read错误处理 + JWT随机密钥 + 密码环境变量化 + 前端路由守卫
This commit is contained in:
17
.env.example
Normal file
17
.env.example
Normal file
@@ -0,0 +1,17 @@
|
||||
# feedsystem_video_go 环境变量模板
|
||||
# 复制此文件为 .env 后修改实际值
|
||||
# .env 已被 .gitignore 忽略,不会提交到仓库
|
||||
|
||||
# MySQL
|
||||
MYSQL_ROOT_PASSWORD=123456
|
||||
MYSQL_DATABASE=feedsystem
|
||||
|
||||
# Redis
|
||||
REDIS_PASSWORD=123456
|
||||
|
||||
# RabbitMQ
|
||||
RABBITMQ_USER=admin
|
||||
RABBITMQ_PASS=password123
|
||||
|
||||
# JWT (生产环境务必修改为随机强密钥)
|
||||
JWT_SECRET=change-me-in-production
|
||||
@@ -1,65 +1,74 @@
|
||||
// internal/auth/jwt.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
func jwtSecret() []byte {
|
||||
secret := os.Getenv("JWT_SECRET")
|
||||
if secret == "" {
|
||||
secret = "change-me-in-env"
|
||||
}
|
||||
return []byte(secret)
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
AccountID uint `json:"account_id"`
|
||||
Username string `json:"username"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateToken(accountID uint, username string) (string, error) {
|
||||
now := time.Now()
|
||||
|
||||
claims := Claims{
|
||||
AccountID: accountID,
|
||||
Username: username,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
return token.SignedString(jwtSecret())
|
||||
}
|
||||
|
||||
func ParseToken(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(
|
||||
tokenString,
|
||||
&Claims{},
|
||||
func(token *jwt.Token) (interface{}, error) {
|
||||
if token.Method == nil || token.Method.Alg() != jwt.SigningMethodHS256.Alg() {
|
||||
return nil, errors.New("unexpected signing method")
|
||||
}
|
||||
return jwtSecret(), nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, jwt.ErrTokenInvalidClaims
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
// internal/auth/jwt.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
func jwtSecret() []byte {
|
||||
secret := os.Getenv("JWT_SECRET")
|
||||
if secret == "" {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
log.Printf("FATAL: cannot generate JWT secret: %v", err)
|
||||
return []byte("fallback-unsafe-key-change-me")
|
||||
}
|
||||
secret = hex.EncodeToString(b)
|
||||
log.Printf("WARNING: JWT_SECRET not set, generated random key. All tokens invalid on restart.")
|
||||
}
|
||||
return []byte(secret)
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
AccountID uint `json:"account_id"`
|
||||
Username string `json:"username"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateToken(accountID uint, username string) (string, error) {
|
||||
now := time.Now()
|
||||
|
||||
claims := Claims{
|
||||
AccountID: accountID,
|
||||
Username: username,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
return token.SignedString(jwtSecret())
|
||||
}
|
||||
|
||||
func ParseToken(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(
|
||||
tokenString,
|
||||
&Claims{},
|
||||
func(token *jwt.Token) (interface{}, error) {
|
||||
if token.Method == nil || token.Method.Alg() != jwt.SigningMethodHS256.Alg() {
|
||||
return nil, errors.New("unexpected signing method")
|
||||
}
|
||||
return jwtSecret(), nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, jwt.ErrTokenInvalidClaims
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
@@ -1,241 +1,253 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"feedsystem_video_go/internal/account"
|
||||
"feedsystem_video_go/internal/middleware/jwt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type VideoHandler struct {
|
||||
service *VideoService
|
||||
accountService *account.AccountService
|
||||
}
|
||||
|
||||
func NewVideoHandler(service *VideoService, accountService *account.AccountService) *VideoHandler {
|
||||
return &VideoHandler{service: service, accountService: accountService}
|
||||
}
|
||||
|
||||
func (vh *VideoHandler) PublishVideo(c *gin.Context) {
|
||||
var req PublishVideoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
authorId, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
username, err := jwt.GetUsername(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
video := &Video{
|
||||
AuthorID: authorId,
|
||||
Username: username,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
PlayURL: req.PlayURL,
|
||||
CoverURL: req.CoverURL,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
if err := vh.service.Publish(c.Request.Context(), video); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, video)
|
||||
}
|
||||
|
||||
func (vh *VideoHandler) UploadVideo(c *gin.Context) {
|
||||
authorId, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
f, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file"})
|
||||
return
|
||||
}
|
||||
|
||||
const maxSize = 200 << 20
|
||||
if f.Size <= 0 || f.Size > maxSize {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file size"})
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(f.Filename))
|
||||
if ext != ".mp4" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "only .mp4 is allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
date := time.Now().Format("20060102")
|
||||
relDir := filepath.Join("videos", fmt.Sprintf("%d", authorId), date)
|
||||
root := filepath.Join(".run", "uploads")
|
||||
absDir := filepath.Join(root, relDir)
|
||||
if err := os.MkdirAll(absDir, 0o755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
filename := randHex(16) + ext
|
||||
absPath := filepath.Join(absDir, filename)
|
||||
|
||||
if err := c.SaveUploadedFile(f, absPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
urlPath := path.Join("/static", "videos", fmt.Sprintf("%d", authorId), date, filename)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"url": buildAbsoluteURL(c, urlPath),
|
||||
"play_url": buildAbsoluteURL(c, urlPath),
|
||||
})
|
||||
}
|
||||
|
||||
func (vh *VideoHandler) UploadCover(c *gin.Context) {
|
||||
authorId, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
f, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file"})
|
||||
return
|
||||
}
|
||||
|
||||
const maxSize = 10 << 20
|
||||
if f.Size <= 0 || f.Size > maxSize {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file size"})
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(f.Filename))
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg", ".png", ".webp":
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "only .jpg/.jpeg/.png/.webp is allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
date := time.Now().Format("20060102")
|
||||
relDir := filepath.Join("covers", fmt.Sprintf("%d", authorId), date)
|
||||
root := filepath.Join(".run", "uploads")
|
||||
absDir := filepath.Join(root, relDir)
|
||||
if err := os.MkdirAll(absDir, 0o755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
filename := randHex(16) + ext
|
||||
absPath := filepath.Join(absDir, filename)
|
||||
|
||||
if err := c.SaveUploadedFile(f, absPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
urlPath := path.Join("/static", "covers", fmt.Sprintf("%d", authorId), date, filename)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"url": buildAbsoluteURL(c, urlPath),
|
||||
"cover_url": buildAbsoluteURL(c, urlPath),
|
||||
})
|
||||
}
|
||||
|
||||
func randHex(n int) string {
|
||||
b := make([]byte, n)
|
||||
_, _ = rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func buildAbsoluteURL(c *gin.Context, p string) string {
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
if xf := c.GetHeader("X-Forwarded-Proto"); xf != "" {
|
||||
scheme = xf
|
||||
}
|
||||
return fmt.Sprintf("%s://%s%s", scheme, c.Request.Host, p)
|
||||
}
|
||||
|
||||
func (vh *VideoHandler) DeleteVideo(c *gin.Context) {
|
||||
var req DeleteVideoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
authorId, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := vh.service.Delete(c.Request.Context(), req.ID, authorId); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "video deleted"})
|
||||
}
|
||||
|
||||
func (vh *VideoHandler) ListByAuthorID(c *gin.Context) {
|
||||
var req ListByAuthorIDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
videos, err := vh.service.ListByAuthorID(c.Request.Context(), req.AuthorID)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if videos == nil {
|
||||
videos = []Video{}
|
||||
}
|
||||
c.JSON(200, videos)
|
||||
}
|
||||
|
||||
func (vh *VideoHandler) GetDetail(c *gin.Context) {
|
||||
var req GetDetailRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
video, err := vh.service.GetDetail(c.Request.Context(), req.ID)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, video)
|
||||
}
|
||||
|
||||
func (vh *VideoHandler) UpdateLikesCount(c *gin.Context) {
|
||||
var req UpdateLikesCountRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := vh.service.UpdateLikesCount(c.Request.Context(), req.ID, req.LikesCount); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "likes count updated"})
|
||||
}
|
||||
package video
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"feedsystem_video_go/internal/account"
|
||||
"feedsystem_video_go/internal/middleware/jwt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type VideoHandler struct {
|
||||
service *VideoService
|
||||
accountService *account.AccountService
|
||||
}
|
||||
|
||||
func NewVideoHandler(service *VideoService, accountService *account.AccountService) *VideoHandler {
|
||||
return &VideoHandler{service: service, accountService: accountService}
|
||||
}
|
||||
|
||||
func (vh *VideoHandler) PublishVideo(c *gin.Context) {
|
||||
var req PublishVideoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
authorId, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
username, err := jwt.GetUsername(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
video := &Video{
|
||||
AuthorID: authorId,
|
||||
Username: username,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
PlayURL: req.PlayURL,
|
||||
CoverURL: req.CoverURL,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
if err := vh.service.Publish(c.Request.Context(), video); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, video)
|
||||
}
|
||||
|
||||
func (vh *VideoHandler) UploadVideo(c *gin.Context) {
|
||||
authorId, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
f, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file"})
|
||||
return
|
||||
}
|
||||
|
||||
const maxSize = 200 << 20
|
||||
if f.Size <= 0 || f.Size > maxSize {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file size"})
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(f.Filename))
|
||||
if ext != ".mp4" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "only .mp4 is allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
date := time.Now().Format("20060102")
|
||||
relDir := filepath.Join("videos", fmt.Sprintf("%d", authorId), date)
|
||||
root := filepath.Join(".run", "uploads")
|
||||
absDir := filepath.Join(root, relDir)
|
||||
if err := os.MkdirAll(absDir, 0o755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
filename, err := randHex(16)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate filename"})
|
||||
return
|
||||
}
|
||||
filename = filename + ext
|
||||
absPath := filepath.Join(absDir, filename)
|
||||
|
||||
if err := c.SaveUploadedFile(f, absPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
urlPath := path.Join("/static", "videos", fmt.Sprintf("%d", authorId), date, filename)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"url": buildAbsoluteURL(c, urlPath),
|
||||
"play_url": buildAbsoluteURL(c, urlPath),
|
||||
})
|
||||
}
|
||||
|
||||
func (vh *VideoHandler) UploadCover(c *gin.Context) {
|
||||
authorId, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
f, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file"})
|
||||
return
|
||||
}
|
||||
|
||||
const maxSize = 10 << 20
|
||||
if f.Size <= 0 || f.Size > maxSize {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file size"})
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(f.Filename))
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg", ".png", ".webp":
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "only .jpg/.jpeg/.png/.webp is allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
date := time.Now().Format("20060102")
|
||||
relDir := filepath.Join("covers", fmt.Sprintf("%d", authorId), date)
|
||||
root := filepath.Join(".run", "uploads")
|
||||
absDir := filepath.Join(root, relDir)
|
||||
if err := os.MkdirAll(absDir, 0o755); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
filename, err := randHex(16)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate filename"})
|
||||
return
|
||||
}
|
||||
filename = filename + ext
|
||||
absPath := filepath.Join(absDir, filename)
|
||||
|
||||
if err := c.SaveUploadedFile(f, absPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
urlPath := path.Join("/static", "covers", fmt.Sprintf("%d", authorId), date, filename)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"url": buildAbsoluteURL(c, urlPath),
|
||||
"cover_url": buildAbsoluteURL(c, urlPath),
|
||||
})
|
||||
}
|
||||
|
||||
func randHex(n int) (string, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", fmt.Errorf("rand.Read: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func buildAbsoluteURL(c *gin.Context, p string) string {
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
if xf := c.GetHeader("X-Forwarded-Proto"); xf != "" {
|
||||
scheme = xf
|
||||
}
|
||||
return fmt.Sprintf("%s://%s%s", scheme, c.Request.Host, p)
|
||||
}
|
||||
|
||||
func (vh *VideoHandler) DeleteVideo(c *gin.Context) {
|
||||
var req DeleteVideoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
authorId, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := vh.service.Delete(c.Request.Context(), req.ID, authorId); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "video deleted"})
|
||||
}
|
||||
|
||||
func (vh *VideoHandler) ListByAuthorID(c *gin.Context) {
|
||||
var req ListByAuthorIDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
videos, err := vh.service.ListByAuthorID(c.Request.Context(), req.AuthorID)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if videos == nil {
|
||||
videos = []Video{}
|
||||
}
|
||||
c.JSON(200, videos)
|
||||
}
|
||||
|
||||
func (vh *VideoHandler) GetDetail(c *gin.Context) {
|
||||
var req GetDetailRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
video, err := vh.service.GetDetail(c.Request.Context(), req.ID)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, video)
|
||||
}
|
||||
|
||||
func (vh *VideoHandler) UpdateLikesCount(c *gin.Context) {
|
||||
var req UpdateLikesCountRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := vh.service.UpdateLikesCount(c.Request.Context(), req.ID, req.LikesCount); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "likes count updated"})
|
||||
}
|
||||
|
||||
@@ -1,106 +1,106 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: "123456"
|
||||
MYSQL_DATABASE: "feedsystem"
|
||||
TZ: "Asia/Shanghai"
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
command:
|
||||
- --default-authentication-plugin=mysql_native_password
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_0900_ai_ci
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p123456 --silent"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: always
|
||||
command: ["redis-server", "--appendonly", "yes", "--requirepass", "123456"]
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a", "123456", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
|
||||
rabbitmq:
|
||||
image: rabbitmq:3-management
|
||||
restart: always
|
||||
ports:
|
||||
- "5672:5672"
|
||||
- "15672:15672"
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: admin
|
||||
RABBITMQ_DEFAULT_PASS: password123
|
||||
volumes:
|
||||
- rabbitmq_data:/var/lib/rabbitmq
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "rabbitmq-diagnostics -q ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: backend/Dockerfile
|
||||
target: api
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./backend/configs/config.docker.yaml:/app/configs/config.yaml:ro
|
||||
- backend_uploads:/app/.run/uploads
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
|
||||
worker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: backend/Dockerfile
|
||||
target: worker
|
||||
restart: always
|
||||
volumes:
|
||||
- ./backend/configs/config.docker.yaml:/app/configs/config.yaml:ro
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: frontend/Dockerfile
|
||||
restart: always
|
||||
ports:
|
||||
- "5173:80"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
redis_data:
|
||||
rabbitmq_data:
|
||||
backend_uploads:
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-123456}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-feedsystem}
|
||||
TZ: "Asia/Shanghai"
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
command:
|
||||
- --default-authentication-plugin=mysql_native_password
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_0900_ai_ci
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p123456 --silent"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: always
|
||||
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:-123456}"]
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a", "123456", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
|
||||
rabbitmq:
|
||||
image: rabbitmq:3-management
|
||||
restart: always
|
||||
ports:
|
||||
- "5672:5672"
|
||||
- "15672:15672"
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-admin}
|
||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS:-password123}
|
||||
volumes:
|
||||
- rabbitmq_data:/var/lib/rabbitmq
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "rabbitmq-diagnostics -q ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: backend/Dockerfile
|
||||
target: api
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./backend/configs/config.docker.yaml:/app/configs/config.yaml:ro
|
||||
- backend_uploads:/app/.run/uploads
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
|
||||
worker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: backend/Dockerfile
|
||||
target: worker
|
||||
restart: always
|
||||
volumes:
|
||||
- ./backend/configs/config.docker.yaml:/app/configs/config.yaml:ro
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: frontend/Dockerfile
|
||||
restart: always
|
||||
ports:
|
||||
- "5173:80"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
redis_data:
|
||||
rabbitmq_data:
|
||||
backend_uploads:
|
||||
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import HotView from '../views/HotView.vue'
|
||||
import VideoView from '../views/VideoView.vue'
|
||||
import VideoDetailView from '../views/VideoDetailView.vue'
|
||||
import AccountView from '../views/AccountView.vue'
|
||||
import ChangePasswordView from '../views/ChangePasswordView.vue'
|
||||
import RegisterView from '../views/RegisterView.vue'
|
||||
import SettingsView from '../views/SettingsView.vue'
|
||||
import UserProfileView from '../views/UserProfileView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'home', component: HomeView },
|
||||
{ path: '/feed', redirect: '/' },
|
||||
{ path: '/hot', name: 'hot', component: HotView },
|
||||
{ path: '/video', name: 'video', component: VideoView },
|
||||
{ path: '/video/:id', name: 'video-detail', component: VideoDetailView, props: true },
|
||||
{ path: '/account', name: 'account', component: AccountView },
|
||||
{ path: '/account/register', name: 'account-register', component: RegisterView },
|
||||
{ path: '/account/change-password', name: 'account-change-password', component: ChangePasswordView },
|
||||
{ path: '/settings', name: 'settings', component: SettingsView },
|
||||
{ path: '/u/:id', name: 'user-profile', component: UserProfileView, props: true },
|
||||
],
|
||||
})
|
||||
|
||||
export default router
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import HotView from '../views/HotView.vue'
|
||||
import VideoView from '../views/VideoView.vue'
|
||||
import VideoDetailView from '../views/VideoDetailView.vue'
|
||||
import AccountView from '../views/AccountView.vue'
|
||||
import ChangePasswordView from '../views/ChangePasswordView.vue'
|
||||
import RegisterView from '../views/RegisterView.vue'
|
||||
import SettingsView from '../views/SettingsView.vue'
|
||||
import UserProfileView from '../views/UserProfileView.vue'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'home', component: HomeView },
|
||||
{ path: '/feed', redirect: '/' },
|
||||
{ path: '/hot', name: 'hot', component: HotView },
|
||||
{ path: '/video', name: 'video', component: VideoView, meta: { requiresAuth: true } },
|
||||
{ path: '/video/:id', name: 'video-detail', component: VideoDetailView, props: true },
|
||||
{ path: '/account', name: 'account', component: AccountView },
|
||||
{ path: '/account/register', name: 'account-register', component: RegisterView },
|
||||
{ path: '/account/change-password', name: 'account-change-password', component: ChangePasswordView },
|
||||
{ path: '/settings', name: 'settings', component: SettingsView, meta: { requiresAuth: true } },
|
||||
{ path: '/u/:id', name: 'user-profile', component: UserProfileView, props: true },
|
||||
],
|
||||
})
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const auth = useAuthStore()
|
||||
if (to.meta.requiresAuth && !auth.isLoggedIn) {
|
||||
next({ path: '/account', query: { redirect: to.fullPath } })
|
||||
return
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
Reference in New Issue
Block a user