refactor(P2): Handler 错误码精确化 — service 层引入哨兵错误,handler 层用 ClassifyHTTPStatus 分类
This commit is contained in:
@@ -1,142 +1,144 @@
|
||||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AccountHandler struct {
|
||||
accountService *AccountService
|
||||
}
|
||||
|
||||
func NewAccountHandler(accountService *AccountService) *AccountHandler {
|
||||
return &AccountHandler{accountService: accountService}
|
||||
}
|
||||
func (h *AccountHandler) CreateAccount(c *gin.Context) {
|
||||
var req CreateAccountRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.accountService.CreateAccount(c.Request.Context(), &Account{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
}); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "account created"})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) Rename(c *gin.Context) {
|
||||
var req RenameRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
accountID, err := getAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
token, err := h.accountService.Rename(c.Request.Context(), accountID, req.NewUsername)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNewUsernameRequired) {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ErrUsernameTaken) {
|
||||
c.JSON(409, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(404, gin.H{"error": "account not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"token": token})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) ChangePassword(c *gin.Context) {
|
||||
var req ChangePasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.accountService.ChangePassword(c.Request.Context(), req.Username, req.OldPassword, req.NewPassword); err != nil {
|
||||
c.JSON(400, gin.H{"error": "unsuccessfully password changed"})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "successfully password changed"})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) FindByID(c *gin.Context) {
|
||||
var req FindByIDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if account, err := h.accountService.FindByID(c.Request.Context(), req.ID); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
} else {
|
||||
c.JSON(200, account)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AccountHandler) FindByUsername(c *gin.Context) {
|
||||
var req FindByUsernameRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if account, err := h.accountService.FindByUsername(c.Request.Context(), req.Username); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
} else {
|
||||
c.JSON(200, account)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AccountHandler) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if token, err := h.accountService.Login(c.Request.Context(), req.Username, req.Password); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
} else {
|
||||
c.JSON(200, gin.H{"token": token})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AccountHandler) Logout(c *gin.Context) {
|
||||
accountID, err := getAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.accountService.Logout(c.Request.Context(), accountID); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "account logged out"})
|
||||
}
|
||||
|
||||
func getAccountID(c *gin.Context) (uint, error) {
|
||||
value, exists := c.Get("accountID")
|
||||
if !exists {
|
||||
return 0, errors.New("accountID not found")
|
||||
}
|
||||
id, ok := value.(uint)
|
||||
if !ok {
|
||||
return 0, errors.New("accountID has invalid type")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
package account
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
httputil "feedsystem_video_go/internal/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AccountHandler struct {
|
||||
accountService *AccountService
|
||||
}
|
||||
|
||||
func NewAccountHandler(accountService *AccountService) *AccountHandler {
|
||||
return &AccountHandler{accountService: accountService}
|
||||
}
|
||||
func (h *AccountHandler) CreateAccount(c *gin.Context) {
|
||||
var req CreateAccountRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.accountService.CreateAccount(c.Request.Context(), &Account{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
}); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "account created"})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) Rename(c *gin.Context) {
|
||||
var req RenameRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
accountID, err := getAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
token, err := h.accountService.Rename(c.Request.Context(), accountID, req.NewUsername)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNewUsernameRequired) {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ErrUsernameTaken) {
|
||||
c.JSON(409, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(404, gin.H{"error": "account not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"token": token})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) ChangePassword(c *gin.Context) {
|
||||
var req ChangePasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.accountService.ChangePassword(c.Request.Context(), req.Username, req.OldPassword, req.NewPassword); err != nil {
|
||||
c.JSON(400, gin.H{"error": "unsuccessfully password changed"})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "successfully password changed"})
|
||||
}
|
||||
|
||||
func (h *AccountHandler) FindByID(c *gin.Context) {
|
||||
var req FindByIDRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if account, err := h.accountService.FindByID(c.Request.Context(), req.ID); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
} else {
|
||||
c.JSON(200, account)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AccountHandler) FindByUsername(c *gin.Context) {
|
||||
var req FindByUsernameRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if account, err := h.accountService.FindByUsername(c.Request.Context(), req.Username); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
} else {
|
||||
c.JSON(200, account)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AccountHandler) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if token, err := h.accountService.Login(c.Request.Context(), req.Username, req.Password); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
} else {
|
||||
c.JSON(200, gin.H{"token": token})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AccountHandler) Logout(c *gin.Context) {
|
||||
accountID, err := getAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.accountService.Logout(c.Request.Context(), accountID); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "account logged out"})
|
||||
}
|
||||
|
||||
func getAccountID(c *gin.Context) (uint, error) {
|
||||
value, exists := c.Get("accountID")
|
||||
if !exists {
|
||||
return 0, errors.New("accountID not found")
|
||||
}
|
||||
id, ok := value.(uint)
|
||||
if !ok {
|
||||
return 0, errors.New("accountID has invalid type")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
@@ -1,175 +1,176 @@
|
||||
package feed
|
||||
|
||||
import (
|
||||
"feedsystem_video_go/internal/middleware/jwt"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type FeedHandler struct {
|
||||
service *FeedService
|
||||
}
|
||||
|
||||
func NewFeedHandler(service *FeedService) *FeedHandler {
|
||||
return &FeedHandler{service: service}
|
||||
}
|
||||
|
||||
func (f *FeedHandler) ListLatest(c *gin.Context) {
|
||||
var req ListLatestRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Limit <= 0 || req.Limit > 50 {
|
||||
req.Limit = 10
|
||||
}
|
||||
var latestTime time.Time
|
||||
if req.LatestTime > 0 {
|
||||
latestTime = time.UnixMilli(req.LatestTime)
|
||||
}
|
||||
viewerAccountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
viewerAccountID = 0
|
||||
}
|
||||
feedItems, err := f.service.ListLatest(c.Request.Context(), req.Limit, latestTime, viewerAccountID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
feedItems.VideoList = nonNilFeedVideoItems(feedItems.VideoList)
|
||||
c.JSON(200, feedItems)
|
||||
}
|
||||
|
||||
func (f *FeedHandler) ListLikesCount(c *gin.Context) {
|
||||
var req ListLikesCountRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Limit <= 0 || req.Limit > 50 {
|
||||
req.Limit = 10
|
||||
}
|
||||
|
||||
var cursor *LikesCountCursor
|
||||
if req.LikesCountBefore != nil || req.IDBefore != nil {
|
||||
if req.LikesCountBefore == nil || req.IDBefore == nil {
|
||||
c.JSON(400, gin.H{"error": "likes_count_before and id_before must be provided together"})
|
||||
return
|
||||
}
|
||||
|
||||
likesCountBefore := *req.LikesCountBefore
|
||||
idBefore := *req.IDBefore
|
||||
|
||||
if likesCountBefore < 0 {
|
||||
c.JSON(400, gin.H{"error": "invalid cursor: likes_count_before must be >= 0"})
|
||||
return
|
||||
}
|
||||
if idBefore == 0 {
|
||||
if likesCountBefore != 0 {
|
||||
c.JSON(400, gin.H{"error": "invalid cursor: id_before must be > 0"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
cursor = &LikesCountCursor{
|
||||
LikesCount: likesCountBefore,
|
||||
ID: idBefore,
|
||||
}
|
||||
}
|
||||
}
|
||||
viewerAccountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
viewerAccountID = 0
|
||||
}
|
||||
feedItems, err := f.service.ListLikesCount(c.Request.Context(), req.Limit, cursor, viewerAccountID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
feedItems.VideoList = nonNilFeedVideoItems(feedItems.VideoList)
|
||||
c.JSON(200, feedItems)
|
||||
}
|
||||
|
||||
func (f *FeedHandler) ListByFollowing(c *gin.Context) {
|
||||
var req ListByFollowingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Limit <= 0 || req.Limit > 50 {
|
||||
req.Limit = 10
|
||||
}
|
||||
viewerAccountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
viewerAccountID = 0
|
||||
}
|
||||
var latestTime time.Time
|
||||
if req.LatestTime > 0 {
|
||||
latestTime = time.Unix(req.LatestTime, 0)
|
||||
}
|
||||
feedItems, err := f.service.ListByFollowing(c.Request.Context(), req.Limit, latestTime, viewerAccountID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
feedItems.VideoList = nonNilFeedVideoItems(feedItems.VideoList)
|
||||
c.JSON(200, feedItems)
|
||||
}
|
||||
|
||||
func (f *FeedHandler) ListByPopularity(c *gin.Context) {
|
||||
var req ListByPopularityRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Limit <= 0 || req.Limit > 50 {
|
||||
req.Limit = 10
|
||||
}
|
||||
viewerAccountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
viewerAccountID = 0
|
||||
}
|
||||
|
||||
var latestPopularity int64
|
||||
var latestBefore time.Time
|
||||
var latestIDBefore uint
|
||||
|
||||
if req.LatestPopularity < 0 {
|
||||
c.JSON(400, gin.H{"error": "latest_popularity must be >= 0"})
|
||||
return
|
||||
}
|
||||
|
||||
anyCursor := !req.LatestBefore.IsZero() || req.LatestIDBefore != nil
|
||||
if anyCursor {
|
||||
if req.LatestBefore.IsZero() || req.LatestIDBefore == nil || *req.LatestIDBefore == 0 {
|
||||
c.JSON(400, gin.H{"error": "latest_before and latest_id_before must be provided together"})
|
||||
return
|
||||
}
|
||||
latestPopularity = req.LatestPopularity
|
||||
latestBefore = req.LatestBefore
|
||||
latestIDBefore = *req.LatestIDBefore
|
||||
}
|
||||
resp, err := f.service.ListByPopularity(
|
||||
c.Request.Context(),
|
||||
req.Limit,
|
||||
req.AsOf,
|
||||
req.Offset,
|
||||
viewerAccountID,
|
||||
latestPopularity,
|
||||
latestBefore,
|
||||
latestIDBefore,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
resp.VideoList = nonNilFeedVideoItems(resp.VideoList)
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
|
||||
func nonNilFeedVideoItems(items []FeedVideoItem) []FeedVideoItem {
|
||||
if items == nil {
|
||||
return []FeedVideoItem{}
|
||||
}
|
||||
return items
|
||||
}
|
||||
package feed
|
||||
|
||||
import (
|
||||
"feedsystem_video_go/internal/middleware/jwt"
|
||||
httputil "feedsystem_video_go/internal/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type FeedHandler struct {
|
||||
service *FeedService
|
||||
}
|
||||
|
||||
func NewFeedHandler(service *FeedService) *FeedHandler {
|
||||
return &FeedHandler{service: service}
|
||||
}
|
||||
|
||||
func (f *FeedHandler) ListLatest(c *gin.Context) {
|
||||
var req ListLatestRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Limit <= 0 || req.Limit > 50 {
|
||||
req.Limit = 10
|
||||
}
|
||||
var latestTime time.Time
|
||||
if req.LatestTime > 0 {
|
||||
latestTime = time.UnixMilli(req.LatestTime)
|
||||
}
|
||||
viewerAccountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
viewerAccountID = 0
|
||||
}
|
||||
feedItems, err := f.service.ListLatest(c.Request.Context(), req.Limit, latestTime, viewerAccountID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
feedItems.VideoList = nonNilFeedVideoItems(feedItems.VideoList)
|
||||
c.JSON(200, feedItems)
|
||||
}
|
||||
|
||||
func (f *FeedHandler) ListLikesCount(c *gin.Context) {
|
||||
var req ListLikesCountRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Limit <= 0 || req.Limit > 50 {
|
||||
req.Limit = 10
|
||||
}
|
||||
|
||||
var cursor *LikesCountCursor
|
||||
if req.LikesCountBefore != nil || req.IDBefore != nil {
|
||||
if req.LikesCountBefore == nil || req.IDBefore == nil {
|
||||
c.JSON(400, gin.H{"error": "likes_count_before and id_before must be provided together"})
|
||||
return
|
||||
}
|
||||
|
||||
likesCountBefore := *req.LikesCountBefore
|
||||
idBefore := *req.IDBefore
|
||||
|
||||
if likesCountBefore < 0 {
|
||||
c.JSON(400, gin.H{"error": "invalid cursor: likes_count_before must be >= 0"})
|
||||
return
|
||||
}
|
||||
if idBefore == 0 {
|
||||
if likesCountBefore != 0 {
|
||||
c.JSON(400, gin.H{"error": "invalid cursor: id_before must be > 0"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
cursor = &LikesCountCursor{
|
||||
LikesCount: likesCountBefore,
|
||||
ID: idBefore,
|
||||
}
|
||||
}
|
||||
}
|
||||
viewerAccountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
viewerAccountID = 0
|
||||
}
|
||||
feedItems, err := f.service.ListLikesCount(c.Request.Context(), req.Limit, cursor, viewerAccountID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
feedItems.VideoList = nonNilFeedVideoItems(feedItems.VideoList)
|
||||
c.JSON(200, feedItems)
|
||||
}
|
||||
|
||||
func (f *FeedHandler) ListByFollowing(c *gin.Context) {
|
||||
var req ListByFollowingRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Limit <= 0 || req.Limit > 50 {
|
||||
req.Limit = 10
|
||||
}
|
||||
viewerAccountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
viewerAccountID = 0
|
||||
}
|
||||
var latestTime time.Time
|
||||
if req.LatestTime > 0 {
|
||||
latestTime = time.Unix(req.LatestTime, 0)
|
||||
}
|
||||
feedItems, err := f.service.ListByFollowing(c.Request.Context(), req.Limit, latestTime, viewerAccountID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
feedItems.VideoList = nonNilFeedVideoItems(feedItems.VideoList)
|
||||
c.JSON(200, feedItems)
|
||||
}
|
||||
|
||||
func (f *FeedHandler) ListByPopularity(c *gin.Context) {
|
||||
var req ListByPopularityRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Limit <= 0 || req.Limit > 50 {
|
||||
req.Limit = 10
|
||||
}
|
||||
viewerAccountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
viewerAccountID = 0
|
||||
}
|
||||
|
||||
var latestPopularity int64
|
||||
var latestBefore time.Time
|
||||
var latestIDBefore uint
|
||||
|
||||
if req.LatestPopularity < 0 {
|
||||
c.JSON(400, gin.H{"error": "latest_popularity must be >= 0"})
|
||||
return
|
||||
}
|
||||
|
||||
anyCursor := !req.LatestBefore.IsZero() || req.LatestIDBefore != nil
|
||||
if anyCursor {
|
||||
if req.LatestBefore.IsZero() || req.LatestIDBefore == nil || *req.LatestIDBefore == 0 {
|
||||
c.JSON(400, gin.H{"error": "latest_before and latest_id_before must be provided together"})
|
||||
return
|
||||
}
|
||||
latestPopularity = req.LatestPopularity
|
||||
latestBefore = req.LatestBefore
|
||||
latestIDBefore = *req.LatestIDBefore
|
||||
}
|
||||
resp, err := f.service.ListByPopularity(
|
||||
c.Request.Context(),
|
||||
req.Limit,
|
||||
req.AsOf,
|
||||
req.Offset,
|
||||
viewerAccountID,
|
||||
latestPopularity,
|
||||
latestBefore,
|
||||
latestIDBefore,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
resp.VideoList = nonNilFeedVideoItems(resp.VideoList)
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
|
||||
func nonNilFeedVideoItems(items []FeedVideoItem) []FeedVideoItem {
|
||||
if items == nil {
|
||||
return []FeedVideoItem{}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
28
backend/internal/http/errors.go
Normal file
28
backend/internal/http/errors.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrValidation = errors.New("validation error")
|
||||
)
|
||||
|
||||
func ClassifyHTTPStatus(err error) int {
|
||||
switch {
|
||||
case err == nil:
|
||||
return http.StatusOK
|
||||
case errors.Is(err, ErrUnauthorized):
|
||||
return http.StatusUnauthorized
|
||||
case errors.Is(err, ErrValidation):
|
||||
return http.StatusBadRequest
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
return http.StatusNotFound
|
||||
default:
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
@@ -1,125 +1,126 @@
|
||||
package social
|
||||
|
||||
import (
|
||||
"feedsystem_video_go/internal/account"
|
||||
"feedsystem_video_go/internal/middleware/jwt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SocialHandler struct {
|
||||
service *SocialService
|
||||
}
|
||||
|
||||
func NewSocialHandler(service *SocialService) *SocialHandler {
|
||||
return &SocialHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *SocialHandler) Follow(c *gin.Context) {
|
||||
var req FollowRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.VloggerID <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "vlogger_id is required"})
|
||||
return
|
||||
}
|
||||
FollowerID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
social := &Social{
|
||||
FollowerID: FollowerID,
|
||||
VloggerID: req.VloggerID,
|
||||
}
|
||||
if err := h.service.Follow(c.Request.Context(), social); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "followed"})
|
||||
}
|
||||
|
||||
func (h *SocialHandler) Unfollow(c *gin.Context) {
|
||||
var req UnfollowRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.VloggerID <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "vlogger_id is required"})
|
||||
return
|
||||
}
|
||||
FollowerID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
social := &Social{
|
||||
FollowerID: FollowerID,
|
||||
VloggerID: req.VloggerID,
|
||||
}
|
||||
if err := h.service.Unfollow(c.Request.Context(), social); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "unfollowed"})
|
||||
}
|
||||
|
||||
func (h *SocialHandler) GetAllFollowers(c *gin.Context) {
|
||||
var req GetAllFollowersRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
vloggerID := req.VloggerID
|
||||
if vloggerID == 0 {
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
vloggerID = accountID
|
||||
}
|
||||
|
||||
followers, err := h.service.GetAllFollowers(c.Request.Context(), vloggerID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if followers == nil {
|
||||
followers = []*account.Account{}
|
||||
}
|
||||
c.JSON(http.StatusOK, GetAllFollowersResponse{Followers: followers})
|
||||
}
|
||||
|
||||
func (h *SocialHandler) GetAllVloggers(c *gin.Context) {
|
||||
var req GetAllVloggersRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
followerID := req.FollowerID
|
||||
if followerID == 0 {
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
followerID = accountID
|
||||
}
|
||||
|
||||
vloggers, err := h.service.GetAllVloggers(c.Request.Context(), followerID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if vloggers == nil {
|
||||
vloggers = []*account.Account{}
|
||||
}
|
||||
c.JSON(http.StatusOK, GetAllVloggersResponse{Vloggers: vloggers})
|
||||
}
|
||||
package social
|
||||
|
||||
import (
|
||||
"feedsystem_video_go/internal/account"
|
||||
httputil "feedsystem_video_go/internal/http"
|
||||
"feedsystem_video_go/internal/middleware/jwt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SocialHandler struct {
|
||||
service *SocialService
|
||||
}
|
||||
|
||||
func NewSocialHandler(service *SocialService) *SocialHandler {
|
||||
return &SocialHandler{service: service}
|
||||
}
|
||||
|
||||
func (h *SocialHandler) Follow(c *gin.Context) {
|
||||
var req FollowRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.VloggerID <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "vlogger_id is required"})
|
||||
return
|
||||
}
|
||||
FollowerID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
social := &Social{
|
||||
FollowerID: FollowerID,
|
||||
VloggerID: req.VloggerID,
|
||||
}
|
||||
if err := h.service.Follow(c.Request.Context(), social); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "followed"})
|
||||
}
|
||||
|
||||
func (h *SocialHandler) Unfollow(c *gin.Context) {
|
||||
var req UnfollowRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.VloggerID <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "vlogger_id is required"})
|
||||
return
|
||||
}
|
||||
FollowerID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
social := &Social{
|
||||
FollowerID: FollowerID,
|
||||
VloggerID: req.VloggerID,
|
||||
}
|
||||
if err := h.service.Unfollow(c.Request.Context(), social); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "unfollowed"})
|
||||
}
|
||||
|
||||
func (h *SocialHandler) GetAllFollowers(c *gin.Context) {
|
||||
var req GetAllFollowersRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
vloggerID := req.VloggerID
|
||||
if vloggerID == 0 {
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
vloggerID = accountID
|
||||
}
|
||||
|
||||
followers, err := h.service.GetAllFollowers(c.Request.Context(), vloggerID)
|
||||
if err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if followers == nil {
|
||||
followers = []*account.Account{}
|
||||
}
|
||||
c.JSON(http.StatusOK, GetAllFollowersResponse{Followers: followers})
|
||||
}
|
||||
|
||||
func (h *SocialHandler) GetAllVloggers(c *gin.Context) {
|
||||
var req GetAllVloggersRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
followerID := req.FollowerID
|
||||
if followerID == 0 {
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
followerID = accountID
|
||||
}
|
||||
|
||||
vloggers, err := h.service.GetAllVloggers(c.Request.Context(), followerID)
|
||||
if err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if vloggers == nil {
|
||||
vloggers = []*account.Account{}
|
||||
}
|
||||
c.JSON(http.StatusOK, GetAllVloggersResponse{Vloggers: vloggers})
|
||||
}
|
||||
|
||||
@@ -1,97 +1,98 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"feedsystem_video_go/internal/account"
|
||||
"feedsystem_video_go/internal/middleware/jwt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CommentHandler struct {
|
||||
service *CommentService
|
||||
accountService *account.AccountService
|
||||
}
|
||||
|
||||
func NewCommentHandler(service *CommentService, accountService *account.AccountService) *CommentHandler {
|
||||
return &CommentHandler{service: service, accountService: accountService}
|
||||
}
|
||||
func (h *CommentHandler) PublishComment(c *gin.Context) {
|
||||
var req PublishCommentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Content == "" {
|
||||
c.JSON(400, gin.H{"error": "content is required"})
|
||||
return
|
||||
}
|
||||
if req.VideoID <= 0 {
|
||||
c.JSON(400, gin.H{"error": "video_id is required"})
|
||||
return
|
||||
}
|
||||
authorId, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
user, err := h.accountService.FindByID(c.Request.Context(), authorId)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
comment := &Comment{
|
||||
Username: user.Username,
|
||||
VideoID: req.VideoID,
|
||||
AuthorID: authorId,
|
||||
Content: req.Content,
|
||||
}
|
||||
if err := h.service.Publish(c.Request.Context(), comment); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "comment published successfully"})
|
||||
}
|
||||
|
||||
func (h *CommentHandler) DeleteComment(c *gin.Context) {
|
||||
var req DeleteCommentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.CommentID <= 0 {
|
||||
c.JSON(400, gin.H{"error": "comment_id is required"})
|
||||
return
|
||||
}
|
||||
if err := h.service.Delete(c.Request.Context(), req.CommentID, accountID); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"message": "comment deleted successfully"})
|
||||
}
|
||||
|
||||
func (h *CommentHandler) GetAllComments(c *gin.Context) {
|
||||
var req GetAllCommentsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.VideoID == 0 {
|
||||
c.JSON(400, gin.H{"error": "video_id is required"})
|
||||
return
|
||||
}
|
||||
comments, err := h.service.GetAll(c.Request.Context(), req.VideoID)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if comments == nil {
|
||||
comments = []Comment{}
|
||||
}
|
||||
c.JSON(200, comments)
|
||||
}
|
||||
package video
|
||||
|
||||
import (
|
||||
"feedsystem_video_go/internal/account"
|
||||
httputil "feedsystem_video_go/internal/http"
|
||||
"feedsystem_video_go/internal/middleware/jwt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CommentHandler struct {
|
||||
service *CommentService
|
||||
accountService *account.AccountService
|
||||
}
|
||||
|
||||
func NewCommentHandler(service *CommentService, accountService *account.AccountService) *CommentHandler {
|
||||
return &CommentHandler{service: service, accountService: accountService}
|
||||
}
|
||||
func (h *CommentHandler) PublishComment(c *gin.Context) {
|
||||
var req PublishCommentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Content == "" {
|
||||
c.JSON(400, gin.H{"error": "content is required"})
|
||||
return
|
||||
}
|
||||
if req.VideoID <= 0 {
|
||||
c.JSON(400, gin.H{"error": "video_id is required"})
|
||||
return
|
||||
}
|
||||
authorId, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
user, err := h.accountService.FindByID(c.Request.Context(), authorId)
|
||||
if err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
comment := &Comment{
|
||||
Username: user.Username,
|
||||
VideoID: req.VideoID,
|
||||
AuthorID: authorId,
|
||||
Content: req.Content,
|
||||
}
|
||||
if err := h.service.Publish(c.Request.Context(), comment); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "comment published successfully"})
|
||||
}
|
||||
|
||||
func (h *CommentHandler) DeleteComment(c *gin.Context) {
|
||||
var req DeleteCommentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.CommentID <= 0 {
|
||||
c.JSON(400, gin.H{"error": "comment_id is required"})
|
||||
return
|
||||
}
|
||||
if err := h.service.Delete(c.Request.Context(), req.CommentID, accountID); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"message": "comment deleted successfully"})
|
||||
}
|
||||
|
||||
func (h *CommentHandler) GetAllComments(c *gin.Context) {
|
||||
var req GetAllCommentsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.VideoID == 0 {
|
||||
c.JSON(400, gin.H{"error": "video_id is required"})
|
||||
return
|
||||
}
|
||||
comments, err := h.service.GetAll(c.Request.Context(), req.VideoID)
|
||||
if err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if comments == nil {
|
||||
comments = []Comment{}
|
||||
}
|
||||
c.JSON(200, comments)
|
||||
}
|
||||
|
||||
@@ -1,116 +1,117 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"feedsystem_video_go/internal/middleware/rabbitmq"
|
||||
rediscache "feedsystem_video_go/internal/middleware/redis"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CommentService struct {
|
||||
repo *CommentRepository
|
||||
VideoRepository *VideoRepository
|
||||
cache *rediscache.Client
|
||||
commentMQ *rabbitmq.CommentMQ
|
||||
popularityMQ *rabbitmq.PopularityMQ
|
||||
}
|
||||
|
||||
func NewCommentService(repo *CommentRepository, videoRepo *VideoRepository, cache *rediscache.Client, commentMQ *rabbitmq.CommentMQ, popularityMQ *rabbitmq.PopularityMQ) *CommentService {
|
||||
return &CommentService{repo: repo, VideoRepository: videoRepo, cache: cache, commentMQ: commentMQ, popularityMQ: popularityMQ}
|
||||
}
|
||||
|
||||
func (s *CommentService) Publish(ctx context.Context, comment *Comment) error {
|
||||
if comment == nil {
|
||||
return errors.New("comment is nil")
|
||||
}
|
||||
comment.Username = strings.TrimSpace(comment.Username)
|
||||
comment.Content = strings.TrimSpace(comment.Content)
|
||||
if comment.VideoID == 0 || comment.AuthorID == 0 {
|
||||
return errors.New("video_id and author_id are required")
|
||||
}
|
||||
if comment.Content == "" {
|
||||
return errors.New("content is required")
|
||||
}
|
||||
|
||||
exists, err := s.VideoRepository.IsExist(ctx, comment.VideoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return errors.New("video not found")
|
||||
}
|
||||
|
||||
mysqlEnqueued := false
|
||||
redisEnqueued := false
|
||||
if s.commentMQ != nil {
|
||||
if err := s.commentMQ.Publish(ctx, comment.Username, comment.VideoID, comment.AuthorID, comment.Content); err == nil {
|
||||
mysqlEnqueued = true
|
||||
}
|
||||
}
|
||||
if s.popularityMQ != nil {
|
||||
if err := s.popularityMQ.Update(ctx, comment.VideoID, 1); err == nil {
|
||||
redisEnqueued = true
|
||||
}
|
||||
}
|
||||
if mysqlEnqueued && redisEnqueued {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fallback: direct MySQL write when comment MQ publish fails.
|
||||
if !mysqlEnqueued {
|
||||
if err := s.repo.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Select("id").First(&Video{}, comment.VideoID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("video not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(comment).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&Video{}).Where("id = ?", comment.VideoID).
|
||||
UpdateColumn("popularity", gorm.Expr("popularity + 1")).Error
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: direct Redis update when popularity MQ publish fails.
|
||||
if !redisEnqueued {
|
||||
UpdatePopularityCache(ctx, s.cache, comment.VideoID, 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CommentService) Delete(ctx context.Context, commentID uint, accountID uint) error {
|
||||
comment, err := s.repo.GetByID(ctx, commentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if comment == nil {
|
||||
return errors.New("comment not found")
|
||||
}
|
||||
if comment.AuthorID != accountID {
|
||||
return errors.New("permission denied")
|
||||
}
|
||||
if s.commentMQ != nil {
|
||||
if err := s.commentMQ.Delete(ctx, commentID); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return s.repo.DeleteComment(ctx, comment)
|
||||
}
|
||||
|
||||
func (s *CommentService) GetAll(ctx context.Context, videoID uint) ([]Comment, error) {
|
||||
exists, err := s.VideoRepository.IsExist(ctx, videoID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, errors.New("video not found")
|
||||
}
|
||||
return s.repo.GetAllComments(ctx, videoID)
|
||||
}
|
||||
package video
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"feedsystem_video_go/internal/middleware/rabbitmq"
|
||||
rediscache "feedsystem_video_go/internal/middleware/redis"
|
||||
httputil "feedsystem_video_go/internal/http"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CommentService struct {
|
||||
repo *CommentRepository
|
||||
VideoRepository *VideoRepository
|
||||
cache *rediscache.Client
|
||||
commentMQ *rabbitmq.CommentMQ
|
||||
popularityMQ *rabbitmq.PopularityMQ
|
||||
}
|
||||
|
||||
func NewCommentService(repo *CommentRepository, videoRepo *VideoRepository, cache *rediscache.Client, commentMQ *rabbitmq.CommentMQ, popularityMQ *rabbitmq.PopularityMQ) *CommentService {
|
||||
return &CommentService{repo: repo, VideoRepository: videoRepo, cache: cache, commentMQ: commentMQ, popularityMQ: popularityMQ}
|
||||
}
|
||||
|
||||
func (s *CommentService) Publish(ctx context.Context, comment *Comment) error {
|
||||
if comment == nil {
|
||||
return errors.New("comment is nil")
|
||||
}
|
||||
comment.Username = strings.TrimSpace(comment.Username)
|
||||
comment.Content = strings.TrimSpace(comment.Content)
|
||||
if comment.VideoID == 0 || comment.AuthorID == 0 {
|
||||
return errors.New("video_id and author_id are required")
|
||||
}
|
||||
if comment.Content == "" {
|
||||
return errors.New("content is required")
|
||||
}
|
||||
|
||||
exists, err := s.VideoRepository.IsExist(ctx, comment.VideoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return errors.New("video not found")
|
||||
}
|
||||
|
||||
mysqlEnqueued := false
|
||||
redisEnqueued := false
|
||||
if s.commentMQ != nil {
|
||||
if err := s.commentMQ.Publish(ctx, comment.Username, comment.VideoID, comment.AuthorID, comment.Content); err == nil {
|
||||
mysqlEnqueued = true
|
||||
}
|
||||
}
|
||||
if s.popularityMQ != nil {
|
||||
if err := s.popularityMQ.Update(ctx, comment.VideoID, 1); err == nil {
|
||||
redisEnqueued = true
|
||||
}
|
||||
}
|
||||
if mysqlEnqueued && redisEnqueued {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fallback: direct MySQL write when comment MQ publish fails.
|
||||
if !mysqlEnqueued {
|
||||
if err := s.repo.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Select("id").First(&Video{}, comment.VideoID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("video not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(comment).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&Video{}).Where("id = ?", comment.VideoID).
|
||||
UpdateColumn("popularity", gorm.Expr("popularity + 1")).Error
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: direct Redis update when popularity MQ publish fails.
|
||||
if !redisEnqueued {
|
||||
UpdatePopularityCache(ctx, s.cache, comment.VideoID, 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CommentService) Delete(ctx context.Context, commentID uint, accountID uint) error {
|
||||
comment, err := s.repo.GetByID(ctx, commentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if comment == nil {
|
||||
return errors.New("comment not found")
|
||||
}
|
||||
if comment.AuthorID != accountID {
|
||||
return httputil.ErrUnauthorized
|
||||
}
|
||||
if s.commentMQ != nil {
|
||||
if err := s.commentMQ.Delete(ctx, commentID); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return s.repo.DeleteComment(ctx, comment)
|
||||
}
|
||||
|
||||
func (s *CommentService) GetAll(ctx context.Context, videoID uint) ([]Comment, error) {
|
||||
exists, err := s.VideoRepository.IsExist(ctx, videoID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, errors.New("video not found")
|
||||
}
|
||||
return s.repo.GetAllComments(ctx, videoID)
|
||||
}
|
||||
|
||||
@@ -1,113 +1,114 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"feedsystem_video_go/internal/middleware/jwt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LikeHandler struct {
|
||||
service *LikeService
|
||||
}
|
||||
|
||||
func NewLikeHandler(service *LikeService) *LikeHandler {
|
||||
return &LikeHandler{service: service}
|
||||
}
|
||||
|
||||
func (lh *LikeHandler) Like(c *gin.Context) {
|
||||
var req LikeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.VideoID <= 0 {
|
||||
c.JSON(400, gin.H{"error": "video_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
like := &Like{
|
||||
VideoID: req.VideoID,
|
||||
AccountID: accountID,
|
||||
}
|
||||
if err := lh.service.Like(c.Request.Context(), like); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "like success"})
|
||||
}
|
||||
|
||||
func (lh *LikeHandler) Unlike(c *gin.Context) {
|
||||
var req LikeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.VideoID <= 0 {
|
||||
c.JSON(400, gin.H{"error": "video_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
like := &Like{
|
||||
VideoID: req.VideoID,
|
||||
AccountID: accountID,
|
||||
}
|
||||
if err := lh.service.Unlike(c.Request.Context(), like); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "unlike success"})
|
||||
}
|
||||
|
||||
func (lh *LikeHandler) IsLiked(c *gin.Context) {
|
||||
var req LikeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.VideoID <= 0 {
|
||||
c.JSON(400, gin.H{"error": "video_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
isLiked, err := lh.service.IsLiked(c.Request.Context(), req.VideoID, accountID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"is_liked": isLiked})
|
||||
}
|
||||
|
||||
func (lh *LikeHandler) ListMyLikedVideos(c *gin.Context) {
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
videos, err := lh.service.ListLikedVideos(c.Request.Context(), accountID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if videos == nil {
|
||||
videos = []Video{}
|
||||
}
|
||||
c.JSON(200, videos)
|
||||
}
|
||||
package video
|
||||
|
||||
import (
|
||||
"feedsystem_video_go/internal/middleware/jwt"
|
||||
httputil "feedsystem_video_go/internal/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LikeHandler struct {
|
||||
service *LikeService
|
||||
}
|
||||
|
||||
func NewLikeHandler(service *LikeService) *LikeHandler {
|
||||
return &LikeHandler{service: service}
|
||||
}
|
||||
|
||||
func (lh *LikeHandler) Like(c *gin.Context) {
|
||||
var req LikeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.VideoID <= 0 {
|
||||
c.JSON(400, gin.H{"error": "video_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
like := &Like{
|
||||
VideoID: req.VideoID,
|
||||
AccountID: accountID,
|
||||
}
|
||||
if err := lh.service.Like(c.Request.Context(), like); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "like success"})
|
||||
}
|
||||
|
||||
func (lh *LikeHandler) Unlike(c *gin.Context) {
|
||||
var req LikeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.VideoID <= 0 {
|
||||
c.JSON(400, gin.H{"error": "video_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
like := &Like{
|
||||
VideoID: req.VideoID,
|
||||
AccountID: accountID,
|
||||
}
|
||||
if err := lh.service.Unlike(c.Request.Context(), like); err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "unlike success"})
|
||||
}
|
||||
|
||||
func (lh *LikeHandler) IsLiked(c *gin.Context) {
|
||||
var req LikeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.VideoID <= 0 {
|
||||
c.JSON(400, gin.H{"error": "video_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
isLiked, err := lh.service.IsLiked(c.Request.Context(), req.VideoID, accountID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"is_liked": isLiked})
|
||||
}
|
||||
|
||||
func (lh *LikeHandler) ListMyLikedVideos(c *gin.Context) {
|
||||
accountID, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
videos, err := lh.service.ListLikedVideos(c.Request.Context(), accountID)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if videos == nil {
|
||||
videos = []Video{}
|
||||
}
|
||||
c.JSON(200, videos)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"feedsystem_video_go/internal/account"
|
||||
httputil "feedsystem_video_go/internal/http"
|
||||
"feedsystem_video_go/internal/middleware/jwt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -29,18 +30,18 @@ func NewVideoHandler(service *VideoService, accountService *account.AccountServi
|
||||
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()})
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
authorId, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
username, err := jwt.GetUsername(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
video := &Video{
|
||||
@@ -53,7 +54,7 @@ func (vh *VideoHandler) PublishVideo(c *gin.Context) {
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
if err := vh.service.Publish(c.Request.Context(), video); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, video)
|
||||
@@ -193,16 +194,16 @@ func buildAbsoluteURL(c *gin.Context, p string) string {
|
||||
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()})
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
authorId, err := jwt.GetAccountID(c)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), 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()})
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "video deleted"})
|
||||
@@ -211,12 +212,12 @@ func (vh *VideoHandler) DeleteVideo(c *gin.Context) {
|
||||
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()})
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), 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()})
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if videos == nil {
|
||||
@@ -228,12 +229,12 @@ func (vh *VideoHandler) ListByAuthorID(c *gin.Context) {
|
||||
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()})
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), 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()})
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, video)
|
||||
@@ -242,11 +243,11 @@ func (vh *VideoHandler) GetDetail(c *gin.Context) {
|
||||
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()})
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), 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()})
|
||||
c.JSON(httputil.ClassifyHTTPStatus(err), gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"message": "likes count updated"})
|
||||
|
||||
@@ -1,220 +1,221 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"feedsystem_video_go/internal/middleware/rabbitmq"
|
||||
rediscache "feedsystem_video_go/internal/middleware/redis"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type VideoService struct {
|
||||
repo *VideoRepository
|
||||
cache *rediscache.Client
|
||||
cacheTTL time.Duration
|
||||
popularityMQ *rabbitmq.PopularityMQ
|
||||
}
|
||||
|
||||
func NewVideoService(repo *VideoRepository, cache *rediscache.Client, popularityMQ *rabbitmq.PopularityMQ) *VideoService {
|
||||
return &VideoService{repo: repo, cache: cache, cacheTTL: 5 * time.Minute, popularityMQ: popularityMQ}
|
||||
}
|
||||
|
||||
func (vs *VideoService) Publish(ctx context.Context, video *Video) error {
|
||||
if video == nil {
|
||||
return errors.New("video is nil")
|
||||
}
|
||||
video.Title = strings.TrimSpace(video.Title)
|
||||
video.PlayURL = strings.TrimSpace(video.PlayURL)
|
||||
video.CoverURL = strings.TrimSpace(video.CoverURL)
|
||||
|
||||
if video.Title == "" {
|
||||
return errors.New("title is required")
|
||||
}
|
||||
if video.PlayURL == "" {
|
||||
return errors.New("play url is required")
|
||||
}
|
||||
if video.CoverURL == "" {
|
||||
return errors.New("cover url is required")
|
||||
}
|
||||
|
||||
//事务保证视频写入库和消息写入本地消息表的一致性
|
||||
err := vs.repo.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(video).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := OutboxMsg{
|
||||
VideoID: video.ID,
|
||||
EventType: "video_published",
|
||||
Status: "pending",
|
||||
CreateTime: video.CreateTime,
|
||||
}
|
||||
|
||||
if err := tx.Create(&msg).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
})
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func (vs *VideoService) Delete(ctx context.Context, id uint, authorID uint) error {
|
||||
video, err := vs.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if video == nil {
|
||||
return errors.New("video not found")
|
||||
}
|
||||
if video.AuthorID != authorID {
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
if err := vs.repo.DeleteVideo(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
if vs.cache != nil {
|
||||
cacheKey := fmt.Sprintf("video:detail:id=%d", id)
|
||||
_ = vs.cache.Del(context.Background(), cacheKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vs *VideoService) ListByAuthorID(ctx context.Context, authorID uint) ([]Video, error) {
|
||||
videos, err := vs.repo.ListByAuthorID(ctx, int64(authorID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return videos, nil
|
||||
}
|
||||
|
||||
func (vs *VideoService) GetDetail(ctx context.Context, id uint) (*Video, error) {
|
||||
cacheKey := fmt.Sprintf("video:detail:id=%d", id)
|
||||
|
||||
getCached := func() (*Video, bool) {
|
||||
opCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
b, err := vs.cache.GetBytes(opCtx, cacheKey)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
var cached Video
|
||||
if err := json.Unmarshal(b, &cached); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &cached, true
|
||||
}
|
||||
|
||||
setCached := func(video *Video) {
|
||||
b, err := json.Marshal(video)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
opCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
_ = vs.cache.SetBytes(opCtx, cacheKey, b, vs.cacheTTL)
|
||||
}
|
||||
|
||||
if vs.cache != nil {
|
||||
if v, ok := getCached(); ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
opCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
b, err := vs.cache.GetBytes(opCtx, cacheKey)
|
||||
cancel()
|
||||
if err == nil {
|
||||
var cached Video
|
||||
if err := json.Unmarshal(b, &cached); err == nil {
|
||||
return &cached, nil
|
||||
}
|
||||
} else if rediscache.IsMiss(err) {
|
||||
lockKey := "lock:" + cacheKey
|
||||
|
||||
lockCtx, lockCancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
token, locked, lockErr := vs.cache.Lock(lockCtx, lockKey, 2*time.Second)
|
||||
lockCancel()
|
||||
|
||||
if lockErr == nil && locked {
|
||||
defer func() { _ = vs.cache.Unlock(context.Background(), lockKey, token) }()
|
||||
|
||||
if v, ok := getCached(); ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
video, err := vs.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setCached(video)
|
||||
return video, nil
|
||||
}
|
||||
|
||||
// 没拿到锁:等待别人回填缓存
|
||||
for i := 0; i < 5; i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(20 * time.Millisecond):
|
||||
}
|
||||
if v, ok := getCached(); ok {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
video, err := vs.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if vs.cache != nil {
|
||||
setCached(video)
|
||||
}
|
||||
return video, nil
|
||||
}
|
||||
|
||||
func (vs *VideoService) UpdateLikesCount(ctx context.Context, id uint, likesCount int64) error {
|
||||
if err := vs.repo.UpdateLikesCount(ctx, id, likesCount); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vs *VideoService) UpdatePopularity(ctx context.Context, id uint, change int64) error {
|
||||
if err := vs.repo.UpdatePopularity(ctx, id, change); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if vs.popularityMQ != nil {
|
||||
if err := vs.popularityMQ.Update(ctx, id, change); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if vs.cache != nil {
|
||||
// 1) 详情缓存:直接失效(最简单靠谱)
|
||||
_ = vs.cache.Del(context.Background(), fmt.Sprintf("video:detail:id=%d", id))
|
||||
|
||||
// 2) 热榜:写到“时间窗ZSET”,不要用 detail key
|
||||
now := time.Now().UTC().Truncate(time.Minute)
|
||||
windowKey := "hot:video:1m:" + now.Format("200601021504")
|
||||
member := strconv.FormatUint(uint64(id), 10)
|
||||
|
||||
opCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
_ = vs.cache.ZincrBy(opCtx, windowKey, member, float64(change))
|
||||
_ = vs.cache.Expire(opCtx, windowKey, 2*time.Hour)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
package video
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"feedsystem_video_go/internal/middleware/rabbitmq"
|
||||
rediscache "feedsystem_video_go/internal/middleware/redis"
|
||||
httputil "feedsystem_video_go/internal/http"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type VideoService struct {
|
||||
repo *VideoRepository
|
||||
cache *rediscache.Client
|
||||
cacheTTL time.Duration
|
||||
popularityMQ *rabbitmq.PopularityMQ
|
||||
}
|
||||
|
||||
func NewVideoService(repo *VideoRepository, cache *rediscache.Client, popularityMQ *rabbitmq.PopularityMQ) *VideoService {
|
||||
return &VideoService{repo: repo, cache: cache, cacheTTL: 5 * time.Minute, popularityMQ: popularityMQ}
|
||||
}
|
||||
|
||||
func (vs *VideoService) Publish(ctx context.Context, video *Video) error {
|
||||
if video == nil {
|
||||
return errors.New("video is nil")
|
||||
}
|
||||
video.Title = strings.TrimSpace(video.Title)
|
||||
video.PlayURL = strings.TrimSpace(video.PlayURL)
|
||||
video.CoverURL = strings.TrimSpace(video.CoverURL)
|
||||
|
||||
if video.Title == "" {
|
||||
return errors.New("title is required")
|
||||
}
|
||||
if video.PlayURL == "" {
|
||||
return errors.New("play url is required")
|
||||
}
|
||||
if video.CoverURL == "" {
|
||||
return errors.New("cover url is required")
|
||||
}
|
||||
|
||||
//事务保证视频写入库和消息写入本地消息表的一致性
|
||||
err := vs.repo.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(video).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := OutboxMsg{
|
||||
VideoID: video.ID,
|
||||
EventType: "video_published",
|
||||
Status: "pending",
|
||||
CreateTime: video.CreateTime,
|
||||
}
|
||||
|
||||
if err := tx.Create(&msg).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
})
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
func (vs *VideoService) Delete(ctx context.Context, id uint, authorID uint) error {
|
||||
video, err := vs.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if video == nil {
|
||||
return errors.New("video not found")
|
||||
}
|
||||
if video.AuthorID != authorID {
|
||||
return httputil.ErrUnauthorized
|
||||
}
|
||||
if err := vs.repo.DeleteVideo(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
if vs.cache != nil {
|
||||
cacheKey := fmt.Sprintf("video:detail:id=%d", id)
|
||||
_ = vs.cache.Del(context.Background(), cacheKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vs *VideoService) ListByAuthorID(ctx context.Context, authorID uint) ([]Video, error) {
|
||||
videos, err := vs.repo.ListByAuthorID(ctx, int64(authorID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return videos, nil
|
||||
}
|
||||
|
||||
func (vs *VideoService) GetDetail(ctx context.Context, id uint) (*Video, error) {
|
||||
cacheKey := fmt.Sprintf("video:detail:id=%d", id)
|
||||
|
||||
getCached := func() (*Video, bool) {
|
||||
opCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
b, err := vs.cache.GetBytes(opCtx, cacheKey)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
var cached Video
|
||||
if err := json.Unmarshal(b, &cached); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &cached, true
|
||||
}
|
||||
|
||||
setCached := func(video *Video) {
|
||||
b, err := json.Marshal(video)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
opCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
_ = vs.cache.SetBytes(opCtx, cacheKey, b, vs.cacheTTL)
|
||||
}
|
||||
|
||||
if vs.cache != nil {
|
||||
if v, ok := getCached(); ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
opCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
b, err := vs.cache.GetBytes(opCtx, cacheKey)
|
||||
cancel()
|
||||
if err == nil {
|
||||
var cached Video
|
||||
if err := json.Unmarshal(b, &cached); err == nil {
|
||||
return &cached, nil
|
||||
}
|
||||
} else if rediscache.IsMiss(err) {
|
||||
lockKey := "lock:" + cacheKey
|
||||
|
||||
lockCtx, lockCancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
token, locked, lockErr := vs.cache.Lock(lockCtx, lockKey, 2*time.Second)
|
||||
lockCancel()
|
||||
|
||||
if lockErr == nil && locked {
|
||||
defer func() { _ = vs.cache.Unlock(context.Background(), lockKey, token) }()
|
||||
|
||||
if v, ok := getCached(); ok {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
video, err := vs.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setCached(video)
|
||||
return video, nil
|
||||
}
|
||||
|
||||
// 没拿到锁:等待别人回填缓存
|
||||
for i := 0; i < 5; i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(20 * time.Millisecond):
|
||||
}
|
||||
if v, ok := getCached(); ok {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
video, err := vs.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if vs.cache != nil {
|
||||
setCached(video)
|
||||
}
|
||||
return video, nil
|
||||
}
|
||||
|
||||
func (vs *VideoService) UpdateLikesCount(ctx context.Context, id uint, likesCount int64) error {
|
||||
if err := vs.repo.UpdateLikesCount(ctx, id, likesCount); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vs *VideoService) UpdatePopularity(ctx context.Context, id uint, change int64) error {
|
||||
if err := vs.repo.UpdatePopularity(ctx, id, change); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if vs.popularityMQ != nil {
|
||||
if err := vs.popularityMQ.Update(ctx, id, change); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if vs.cache != nil {
|
||||
// 1) 详情缓存:直接失效(最简单靠谱)
|
||||
_ = vs.cache.Del(context.Background(), fmt.Sprintf("video:detail:id=%d", id))
|
||||
|
||||
// 2) 热榜:写到“时间窗ZSET”,不要用 detail key
|
||||
now := time.Now().UTC().Truncate(time.Minute)
|
||||
windowKey := "hot:video:1m:" + now.Format("200601021504")
|
||||
member := strconv.FormatUint(uint64(id), 10)
|
||||
|
||||
opCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
_ = vs.cache.ZincrBy(opCtx, windowKey, member, float64(change))
|
||||
_ = vs.cache.Expire(opCtx, windowKey, 2*time.Hour)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user