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