fix: 修复构建配置和运行时错误处理
This commit is contained in:
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.git
|
||||||
|
**/node_modules
|
||||||
|
**/dist
|
||||||
|
**/.run
|
||||||
|
**/*.log
|
||||||
|
npm-debug.log*
|
||||||
@@ -11,7 +11,7 @@ ARG GO_VERSION=1.24.5
|
|||||||
ARG GOPROXY=https://goproxy.cn,direct
|
ARG GOPROXY=https://goproxy.cn,direct
|
||||||
ARG GOSUMDB=sum.golang.google.cn
|
ARG GOSUMDB=sum.golang.google.cn
|
||||||
|
|
||||||
FROM golang:${GO_VERSION} AS build
|
FROM golang:${GO_VERSION} AS deps
|
||||||
ARG GOPROXY
|
ARG GOPROXY
|
||||||
ARG GOSUMDB
|
ARG GOSUMDB
|
||||||
ENV GOPROXY=${GOPROXY} \
|
ENV GOPROXY=${GOPROXY} \
|
||||||
@@ -24,12 +24,17 @@ RUN --mount=type=cache,target=/go/pkg/mod \
|
|||||||
--mount=type=cache,target=/root/.cache/go-build \
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
go mod download
|
go mod download
|
||||||
|
|
||||||
|
FROM deps AS source
|
||||||
COPY backend/ ./
|
COPY backend/ ./
|
||||||
|
|
||||||
ENV CGO_ENABLED=0
|
ENV CGO_ENABLED=0
|
||||||
|
|
||||||
|
FROM source AS api-build
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
--mount=type=cache,target=/root/.cache/go-build \
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
go build -trimpath -ldflags="-s -w" -o /out/api ./cmd
|
go build -trimpath -ldflags="-s -w" -o /out/api ./cmd
|
||||||
|
|
||||||
|
FROM source AS worker-build
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
--mount=type=cache,target=/root/.cache/go-build \
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
go build -trimpath -ldflags="-s -w" -o /out/worker ./cmd/worker
|
go build -trimpath -ldflags="-s -w" -o /out/worker ./cmd/worker
|
||||||
@@ -37,15 +42,15 @@ RUN --mount=type=cache,target=/go/pkg/mod \
|
|||||||
FROM alpine:3.21 AS base
|
FROM alpine:3.21 AS base
|
||||||
RUN apk add --no-cache ca-certificates tzdata && adduser -D -H -s /sbin/nologin app
|
RUN apk add --no-cache ca-certificates tzdata && adduser -D -H -s /sbin/nologin app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /src/backend/configs ./configs
|
COPY --from=source /src/backend/configs ./configs
|
||||||
RUN mkdir -p ./.run/uploads && chown -R app:app /app
|
RUN mkdir -p ./.run/uploads && chown -R app:app /app
|
||||||
USER app
|
USER app
|
||||||
|
|
||||||
FROM base AS api
|
FROM base AS api
|
||||||
COPY --from=build /out/api /app/api
|
COPY --from=api-build /out/api /app/api
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
ENTRYPOINT ["/app/api"]
|
ENTRYPOINT ["/app/api"]
|
||||||
|
|
||||||
FROM base AS worker
|
FROM base AS worker
|
||||||
COPY --from=build /out/worker /app/worker
|
COPY --from=worker-build /out/worker /app/worker
|
||||||
ENTRYPOINT ["/app/worker"]
|
ENTRYPOINT ["/app/worker"]
|
||||||
|
|||||||
@@ -165,7 +165,9 @@ func (as *AccountService) Logout(ctx context.Context, accountID uint) error {
|
|||||||
log.Printf("failed to del refresh cache: %v", err)
|
log.Printf("failed to del refresh cache: %v", err)
|
||||||
}
|
}
|
||||||
if account.RefreshToken != "" {
|
if account.RefreshToken != "" {
|
||||||
as.cache.Del(cacheCtx, as.cache.Key("refresh:%s", account.RefreshToken))
|
if err := as.cache.Del(cacheCtx, as.cache.Key("refresh:%s", account.RefreshToken)); err != nil {
|
||||||
|
log.Printf("failed to del refresh lookup: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return as.accountRepository.Logout(ctx, account.ID)
|
return as.accountRepository.Logout(ctx, account.ID)
|
||||||
@@ -211,8 +213,12 @@ func (as *AccountService) RefreshAccessToken(ctx context.Context, refreshToken s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, "", err
|
return "", 0, "", err
|
||||||
}
|
}
|
||||||
as.accountRepository.UpdateToken(ctx, account.ID, newToken)
|
if err := as.accountRepository.UpdateToken(ctx, account.ID, newToken); err != nil {
|
||||||
as.cache.SetBytes(cacheCtx, as.cache.Key("account:%d", account.ID), []byte(newToken), 24*time.Hour)
|
return "", 0, "", err
|
||||||
|
}
|
||||||
|
if err := as.cache.SetBytes(cacheCtx, as.cache.Key("account:%d", account.ID), []byte(newToken), 24*time.Hour); err != nil {
|
||||||
|
log.Printf("failed to set cache: %v", err)
|
||||||
|
}
|
||||||
return newToken, account.ID, account.Username, nil
|
return newToken, account.ID, account.Username, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,7 +234,9 @@ func (as *AccountService) RefreshAccessToken(ctx context.Context, refreshToken s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, "", err
|
return "", 0, "", err
|
||||||
}
|
}
|
||||||
as.accountRepository.UpdateToken(ctx, acc.ID, newToken)
|
if err := as.accountRepository.UpdateToken(ctx, acc.ID, newToken); err != nil {
|
||||||
|
return "", 0, "", err
|
||||||
|
}
|
||||||
return newToken, acc.ID, acc.Username, nil
|
return newToken, acc.ID, acc.Username, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,11 +147,15 @@ func (f *FeedService) GetVideoByIDs(ctx context.Context, videoIDs []uint) ([]*vi
|
|||||||
|
|
||||||
// 查询最新视频 (冷热分离 + 游标分页)
|
// 查询最新视频 (冷热分离 + 游标分页)
|
||||||
func (f *FeedService) ListLatest(ctx context.Context, limit int, latestBefore time.Time, viewerAccountID uint) (ListLatestResponse, error) {
|
func (f *FeedService) ListLatest(ctx context.Context, limit int, latestBefore time.Time, viewerAccountID uint) (ListLatestResponse, error) {
|
||||||
|
if f.rediscache == nil {
|
||||||
|
return f.listLatestFromDB(ctx, limit, latestBefore, viewerAccountID)
|
||||||
|
}
|
||||||
|
|
||||||
// 获取 ZSET 中最老的一条数据
|
// 获取 ZSET 中最老的一条数据
|
||||||
zsetTail, err := f.rediscache.ZRangeWithScores(ctx, f.rediscache.Key("feed:global_timeline"), 0, 0)
|
zsetTail, err := f.rediscache.ZRangeWithScores(ctx, f.rediscache.Key("feed:global_timeline"), 0, 0)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ListLatestResponse{}, err
|
return f.listLatestFromDB(ctx, limit, latestBefore, viewerAccountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
isZsetEmpty := len(zsetTail) == 0
|
isZsetEmpty := len(zsetTail) == 0
|
||||||
@@ -271,9 +275,7 @@ func (f *FeedService) ListLatest(ctx context.Context, limit int, latestBefore ti
|
|||||||
// 将本页最后一条视频的时间作为下一次请求的游标
|
// 将本页最后一条视频的时间作为下一次请求的游标
|
||||||
nextTime = baseVideos[len(baseVideos)-1].CreateTime.UnixMilli()
|
nextTime = baseVideos[len(baseVideos)-1].CreateTime.UnixMilli()
|
||||||
}
|
}
|
||||||
var hasMore bool
|
hasMore := len(baseVideos) == limit
|
||||||
|
|
||||||
hasMore = len(baseVideos) == limit
|
|
||||||
|
|
||||||
feedVideos, err := f.buildFeedVideos(ctx, baseVideos, viewerAccountID)
|
feedVideos, err := f.buildFeedVideos(ctx, baseVideos, viewerAccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -287,6 +289,26 @@ func (f *FeedService) ListLatest(ctx context.Context, limit int, latestBefore ti
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FeedService) listLatestFromDB(ctx context.Context, limit int, latestBefore time.Time, viewerAccountID uint) (ListLatestResponse, error) {
|
||||||
|
videos, err := f.repo.ListLatest(ctx, limit, latestBefore)
|
||||||
|
if err != nil {
|
||||||
|
return ListLatestResponse{}, err
|
||||||
|
}
|
||||||
|
feedVideos, err := f.buildFeedVideos(ctx, videos, viewerAccountID)
|
||||||
|
if err != nil {
|
||||||
|
return ListLatestResponse{}, err
|
||||||
|
}
|
||||||
|
var nextTime int64
|
||||||
|
if len(videos) > 0 {
|
||||||
|
nextTime = videos[len(videos)-1].CreateTime.UnixMilli()
|
||||||
|
}
|
||||||
|
return ListLatestResponse{
|
||||||
|
VideoList: feedVideos,
|
||||||
|
NextTime: nextTime,
|
||||||
|
HasMore: len(videos) == limit,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// 按照点赞数查询视频
|
// 按照点赞数查询视频
|
||||||
func (f *FeedService) ListLikesCount(ctx context.Context, limit int, cursor *LikesCountCursor, viewerAccountID uint) (ListLikesCountResponse, error) {
|
func (f *FeedService) ListLikesCount(ctx context.Context, limit int, cursor *LikesCountCursor, viewerAccountID uint) (ListLikesCountResponse, error) {
|
||||||
videos, err := f.repo.ListLikesCountWithCursor(ctx, limit, cursor)
|
videos, err := f.repo.ListLikesCountWithCursor(ctx, limit, cursor)
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import (
|
|||||||
"feedsystem_video_go/internal/social"
|
"feedsystem_video_go/internal/social"
|
||||||
"feedsystem_video_go/internal/video"
|
"feedsystem_video_go/internal/video"
|
||||||
"feedsystem_video_go/internal/worker"
|
"feedsystem_video_go/internal/worker"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetRouter(db *gorm.DB, cache *rediscache.Client, rmq *rabbitmq.RabbitMQ) *gin.Engine {
|
func SetRouter(db *gorm.DB, cache *rediscache.Client, rmq *rabbitmq.RabbitMQ) *gin.Engine {
|
||||||
@@ -23,6 +24,9 @@ func SetRouter(db *gorm.DB, cache *rediscache.Client, rmq *rabbitmq.RabbitMQ) *g
|
|||||||
if err := r.SetTrustedProxies(nil); err != nil {
|
if err := r.SetTrustedProxies(nil); err != nil {
|
||||||
log.Printf("SetTrustedProxies failed: %v", err)
|
log.Printf("SetTrustedProxies failed: %v", err)
|
||||||
}
|
}
|
||||||
|
r.GET("/healthz", func(c *gin.Context) {
|
||||||
|
c.JSON(200, gin.H{"status": "ok"})
|
||||||
|
})
|
||||||
r.Static("/static", "./.run/uploads")
|
r.Static("/static", "./.run/uploads")
|
||||||
// rate_limit
|
// rate_limit
|
||||||
loginLimiter := ratelimit.Limit(cache, "account_login", 10, time.Minute, ratelimit.KeyByIP)
|
loginLimiter := ratelimit.Limit(cache, "account_login", 10, time.Minute, ratelimit.KeyByIP)
|
||||||
@@ -201,9 +205,15 @@ func SetRouter(db *gorm.DB, cache *rediscache.Client, rmq *rabbitmq.RabbitMQ) *g
|
|||||||
|
|
||||||
// SSE notification
|
// SSE notification
|
||||||
if rmq != nil && rmq.Ch != nil {
|
if rmq != nil && rmq.Ch != nil {
|
||||||
rmq.DeclareTopic("like.events", "notification.like", "like.like")
|
if err := rmq.DeclareTopic("like.events", "notification.like", "like.like"); err != nil {
|
||||||
rmq.DeclareTopic("comment.events", "notification.comment", "comment.publish")
|
log.Printf("notification like topic init failed: %v", err)
|
||||||
rmq.DeclareTopic("social.events", "notification.social", "social.follow")
|
}
|
||||||
|
if err := rmq.DeclareTopic("comment.events", "notification.comment", "comment.publish"); err != nil {
|
||||||
|
log.Printf("notification comment topic init failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := rmq.DeclareTopic("social.events", "notification.social", "social.follow"); err != nil {
|
||||||
|
log.Printf("notification social topic init failed: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sseHub := worker.NewSSEHub(db)
|
sseHub := worker.NewSSEHub(db)
|
||||||
notifGroup := r.Group("/notification")
|
notifGroup := r.Group("/notification")
|
||||||
@@ -216,19 +226,37 @@ func SetRouter(db *gorm.DB, cache *rediscache.Client, rmq *rabbitmq.RabbitMQ) *g
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
// consume from like queue
|
// consume from like queue
|
||||||
go func() {
|
go func() {
|
||||||
w := worker.NewNotificationWorker(rmq.Ch, db, "notification.like", hub)
|
ch, err := rmq.Conn.Channel()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("notification-like channel: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer ch.Close()
|
||||||
|
w := worker.NewNotificationWorker(ch, db, "notification.like", hub)
|
||||||
if err := w.Run(ctx); err != nil {
|
if err := w.Run(ctx); err != nil {
|
||||||
log.Printf("notification-like worker: %v", err)
|
log.Printf("notification-like worker: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
w := worker.NewNotificationWorker(rmq.Ch, db, "notification.comment", hub)
|
ch, err := rmq.Conn.Channel()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("notification-comment channel: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer ch.Close()
|
||||||
|
w := worker.NewNotificationWorker(ch, db, "notification.comment", hub)
|
||||||
if err := w.Run(ctx); err != nil {
|
if err := w.Run(ctx); err != nil {
|
||||||
log.Printf("notification-comment worker: %v", err)
|
log.Printf("notification-comment worker: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
w := worker.NewNotificationWorker(rmq.Ch, db, "notification.social", hub)
|
ch, err := rmq.Conn.Channel()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("notification-social channel: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer ch.Close()
|
||||||
|
w := worker.NewNotificationWorker(ch, db, "notification.social", hub)
|
||||||
if err := w.Run(ctx); err != nil {
|
if err := w.Run(ctx); err != nil {
|
||||||
log.Printf("notification-social worker: %v", err)
|
log.Printf("notification-social worker: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,22 +30,28 @@ func NewRabbitMQ(cfg *config.RabbitMQConfig) (*RabbitMQ, error) {
|
|||||||
}
|
}
|
||||||
ch, err := conn.Channel()
|
ch, err := conn.Channel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &RabbitMQ{Conn: conn, Ch: ch}, nil
|
return &RabbitMQ{Conn: conn, Ch: ch}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RabbitMQ) Close() error {
|
func (r *RabbitMQ) Close() error {
|
||||||
if r == nil || r.Ch == nil || r.Conn == nil {
|
if r == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := r.Ch.Close(); err != nil {
|
var closeErr error
|
||||||
return err
|
if r.Ch != nil {
|
||||||
|
if err := r.Ch.Close(); err != nil {
|
||||||
|
closeErr = err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := r.Conn.Close(); err != nil {
|
if r.Conn != nil {
|
||||||
return err
|
if err := r.Conn.Close(); closeErr == nil && err != nil {
|
||||||
|
closeErr = err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return closeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RabbitMQ) DeclareTopic(exchange string, queue string, bindingKey string) error {
|
func (r *RabbitMQ) DeclareTopic(exchange string, queue string, bindingKey string) error {
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import (
|
|||||||
jwt "feedsystem_video_go/internal/middleware/jwt"
|
jwt "feedsystem_video_go/internal/middleware/jwt"
|
||||||
rediscache "feedsystem_video_go/internal/middleware/redis"
|
rediscache "feedsystem_video_go/internal/middleware/redis"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type KeyFunc func(*gin.Context) (string, bool)
|
type KeyFunc func(*gin.Context) (string, bool)
|
||||||
|
|||||||
@@ -2,21 +2,34 @@ package redis
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) GetBytes(ctx context.Context, key string) ([]byte, error) {
|
func (c *Client) GetBytes(ctx context.Context, key string) ([]byte, error) {
|
||||||
|
if c == nil || c.rdb == nil {
|
||||||
|
return nil, errors.New("redis client not initialized")
|
||||||
|
}
|
||||||
return c.rdb.Get(ctx, key).Bytes()
|
return c.rdb.Get(ctx, key).Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SetBytes(ctx context.Context, key string, value []byte, ttl time.Duration) error {
|
func (c *Client) SetBytes(ctx context.Context, key string, value []byte, ttl time.Duration) error {
|
||||||
|
if c == nil || c.rdb == nil {
|
||||||
|
return errors.New("redis client not initialized")
|
||||||
|
}
|
||||||
return c.rdb.Set(ctx, key, value, ttl).Err()
|
return c.rdb.Set(ctx, key, value, ttl).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Del(ctx context.Context, key string) error {
|
func (c *Client) Del(ctx context.Context, key string) error {
|
||||||
|
if c == nil || c.rdb == nil {
|
||||||
|
return errors.New("redis client not initialized")
|
||||||
|
}
|
||||||
return c.rdb.Del(ctx, key).Err()
|
return c.rdb.Del(ctx, key).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) MGet(cacheCtx context.Context, cacheKeys ...string) ([]interface{}, error) {
|
func (c *Client) MGet(cacheCtx context.Context, cacheKeys ...string) ([]interface{}, error) {
|
||||||
|
if c == nil || c.rdb == nil {
|
||||||
|
return nil, errors.New("redis client not initialized")
|
||||||
|
}
|
||||||
return c.rdb.MGet(cacheCtx, cacheKeys...).Result()
|
return c.rdb.MGet(cacheCtx, cacheKeys...).Result()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"feedsystem_video_go/internal/config"
|
"feedsystem_video_go/internal/config"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -41,7 +42,7 @@ func (c *Client) Close() error {
|
|||||||
|
|
||||||
func (c *Client) Ping(ctx context.Context) error {
|
func (c *Client) Ping(ctx context.Context) error {
|
||||||
if c == nil || c.rdb == nil {
|
if c == nil || c.rdb == nil {
|
||||||
return nil
|
return errors.New("redis client not initialized")
|
||||||
}
|
}
|
||||||
return c.rdb.Ping(ctx).Err()
|
return c.rdb.Ping(ctx).Err()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package video
|
|||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -18,6 +19,8 @@ import (
|
|||||||
|
|
||||||
const sessionTTL = 24 * time.Hour
|
const sessionTTL = 24 * time.Hour
|
||||||
|
|
||||||
|
var errChunkCacheUnavailable = errors.New("chunk upload requires redis")
|
||||||
|
|
||||||
type ChunkUploadHandler struct {
|
type ChunkUploadHandler struct {
|
||||||
cache *rediscache.Client
|
cache *rediscache.Client
|
||||||
}
|
}
|
||||||
@@ -35,6 +38,9 @@ func (h *ChunkUploadHandler) hashKey(accountID uint, fileHash string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ChunkUploadHandler) getSession(ctx *gin.Context, uploadID string) (*ChunkUploadSession, error) {
|
func (h *ChunkUploadHandler) getSession(ctx *gin.Context, uploadID string) (*ChunkUploadSession, error) {
|
||||||
|
if h.cache == nil {
|
||||||
|
return nil, errChunkCacheUnavailable
|
||||||
|
}
|
||||||
b, err := h.cache.GetBytes(ctx.Request.Context(), h.sessionKey(uploadID))
|
b, err := h.cache.GetBytes(ctx.Request.Context(), h.sessionKey(uploadID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("upload session not found")
|
return nil, fmt.Errorf("upload session not found")
|
||||||
@@ -47,6 +53,9 @@ func (h *ChunkUploadHandler) getSession(ctx *gin.Context, uploadID string) (*Chu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ChunkUploadHandler) saveSession(ctx *gin.Context, s *ChunkUploadSession) error {
|
func (h *ChunkUploadHandler) saveSession(ctx *gin.Context, s *ChunkUploadSession) error {
|
||||||
|
if h.cache == nil {
|
||||||
|
return errChunkCacheUnavailable
|
||||||
|
}
|
||||||
b, err := json.Marshal(s)
|
b, err := json.Marshal(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -55,6 +64,11 @@ func (h *ChunkUploadHandler) saveSession(ctx *gin.Context, s *ChunkUploadSession
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ChunkUploadHandler) InitChunkUpload(c *gin.Context) {
|
func (h *ChunkUploadHandler) InitChunkUpload(c *gin.Context) {
|
||||||
|
if h.cache == nil {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": errChunkCacheUnavailable.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var req InitChunkUploadRequest
|
var req InitChunkUploadRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
@@ -120,6 +134,11 @@ func (h *ChunkUploadHandler) InitChunkUpload(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ChunkUploadHandler) UploadChunk(c *gin.Context) {
|
func (h *ChunkUploadHandler) UploadChunk(c *gin.Context) {
|
||||||
|
if h.cache == nil {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": errChunkCacheUnavailable.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var req UploadChunkRequest
|
var req UploadChunkRequest
|
||||||
if err := c.ShouldBind(&req); err != nil {
|
if err := c.ShouldBind(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
@@ -211,6 +230,11 @@ func (h *ChunkUploadHandler) UploadChunk(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ChunkUploadHandler) ChunkStatus(c *gin.Context) {
|
func (h *ChunkUploadHandler) ChunkStatus(c *gin.Context) {
|
||||||
|
if h.cache == nil {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": errChunkCacheUnavailable.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var req ChunkStatusRequest
|
var req ChunkStatusRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
@@ -241,6 +265,11 @@ func (h *ChunkUploadHandler) ChunkStatus(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ChunkUploadHandler) CompleteChunkUpload(c *gin.Context) {
|
func (h *ChunkUploadHandler) CompleteChunkUpload(c *gin.Context) {
|
||||||
|
if h.cache == nil {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": errChunkCacheUnavailable.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var req CompleteChunkUploadRequest
|
var req CompleteChunkUploadRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
@@ -328,11 +357,11 @@ func (h *ChunkUploadHandler) CompleteChunkUpload(c *gin.Context) {
|
|||||||
finalFile.Close()
|
finalFile.Close()
|
||||||
|
|
||||||
// Clean up temp chunks
|
// Clean up temp chunks
|
||||||
os.RemoveAll(tmpDir)
|
_ = os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
// Clean up Redis session
|
// Clean up Redis session
|
||||||
h.cache.Del(c.Request.Context(), h.sessionKey(req.UploadID))
|
_ = h.cache.Del(c.Request.Context(), h.sessionKey(req.UploadID))
|
||||||
h.cache.Del(c.Request.Context(), h.hashKey(accountID, session.FileHash))
|
_ = h.cache.Del(c.Request.Context(), h.hashKey(accountID, session.FileHash))
|
||||||
|
|
||||||
urlPath := fmt.Sprintf("/static/videos/%d/%s/%s.mp4", accountID, date, filename)
|
urlPath := fmt.Sprintf("/static/videos/%d/%s/%s.mp4", accountID, date, filename)
|
||||||
playURL := buildAbsoluteURL(c, urlPath)
|
playURL := buildAbsoluteURL(c, urlPath)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"feedsystem_video_go/internal/apierror"
|
"feedsystem_video_go/internal/apierror"
|
||||||
"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"
|
||||||
|
"log"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -150,6 +151,8 @@ func (s *CommentService) notifyMentions(ctx context.Context, comment *Comment) {
|
|||||||
TargetID: comment.VideoID,
|
TargetID: comment.VideoID,
|
||||||
Content: comment.Username + " 在评论中提到了你",
|
Content: comment.Username + " 在评论中提到了你",
|
||||||
}
|
}
|
||||||
s.repo.db.WithContext(ctx).Table("notifications").Create(¬if)
|
if err := s.repo.db.WithContext(ctx).Table("notifications").Create(¬if).Error; err != nil {
|
||||||
|
log.Printf("create mention notification failed: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,8 +64,12 @@ func (vs *VideoService) Publish(ctx context.Context, video *Video) error {
|
|||||||
tags := ExtractTags(video.Title + " " + video.Description)
|
tags := ExtractTags(video.Title + " " + video.Description)
|
||||||
for _, tagName := range tags {
|
for _, tagName := range tags {
|
||||||
var tag Tag
|
var tag Tag
|
||||||
tx.Where("name = ?", tagName).FirstOrCreate(&tag, Tag{Name: tagName})
|
if err := tx.Where("name = ?", tagName).FirstOrCreate(&tag, Tag{Name: tagName}).Error; err != nil {
|
||||||
tx.Create(&VideoTag{VideoID: video.ID, TagID: tag.ID})
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Create(&VideoTag{VideoID: video.ID, TagID: tag.ID}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"feedsystem_video_go/internal/middleware/rabbitmq"
|
"feedsystem_video_go/internal/middleware/rabbitmq"
|
||||||
"feedsystem_video_go/internal/video"
|
"feedsystem_video_go/internal/video"
|
||||||
amqp "github.com/rabbitmq/amqp091-go"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
amqp "github.com/rabbitmq/amqp091-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LikeWorker struct {
|
type LikeWorker struct {
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ func (w *NotificationWorker) Run(ctx context.Context) error {
|
|||||||
if w == nil || w.ch == nil || w.db == nil {
|
if w == nil || w.ch == nil || w.db == nil {
|
||||||
return errors.New("notification worker is not initialized")
|
return errors.New("notification worker is not initialized")
|
||||||
}
|
}
|
||||||
|
if w.queue == "" {
|
||||||
|
return errors.New("queue is required")
|
||||||
|
}
|
||||||
if err := w.db.WithContext(ctx).AutoMigrate(&Notification{}); err != nil {
|
if err := w.db.WithContext(ctx).AutoMigrate(&Notification{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -96,10 +99,12 @@ func (w *NotificationWorker) process(ctx context.Context, d amqp.Delivery) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var authorID uint
|
var authorID uint
|
||||||
w.db.WithContext(ctx).Model(&struct {
|
if err := w.db.WithContext(ctx).Model(&struct {
|
||||||
ID uint
|
ID uint
|
||||||
AuthorID uint
|
AuthorID uint
|
||||||
}{}).Table("videos").Where("id = ?", evt.VideoID).Select("author_id").Scan(&authorID)
|
}{}).Table("videos").Where("id = ?", evt.VideoID).Select("author_id").Scan(&authorID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if authorID == 0 || authorID == evt.UserID {
|
if authorID == 0 || authorID == evt.UserID {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -114,10 +119,12 @@ func (w *NotificationWorker) process(ctx context.Context, d amqp.Delivery) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var authorID uint
|
var authorID uint
|
||||||
w.db.WithContext(ctx).Model(&struct {
|
if err := w.db.WithContext(ctx).Model(&struct {
|
||||||
ID uint
|
ID uint
|
||||||
AuthorID uint
|
AuthorID uint
|
||||||
}{}).Table("videos").Where("id = ?", evt.VideoID).Select("author_id").Scan(&authorID)
|
}{}).Table("videos").Where("id = ?", evt.VideoID).Select("author_id").Scan(&authorID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if authorID == 0 || authorID == evt.AuthorID {
|
if authorID == 0 || authorID == evt.AuthorID {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ func StartOutboxPoller(db *gorm.DB, tmq *rabbitmq.TimelineMQ) {
|
|||||||
err := tmq.PublishVideo(context.Background(), msg.VideoID, msg.CreateTime)
|
err := tmq.PublishVideo(context.Background(), msg.VideoID, msg.CreateTime)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
db.Delete(&msg)
|
if err := db.Delete(&msg).Error; err != nil {
|
||||||
|
log.Printf("删除 outbox 消息失败: id=%d, err=%v", msg.ID, err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("投递MQ失败: VideoID: %d, err: %v", msg.VideoID, err)
|
log.Printf("投递MQ失败: VideoID: %d, err: %v", msg.VideoID, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package worker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -25,8 +27,8 @@ func NewSSEHub(db *gorm.DB) *SSEHub {
|
|||||||
|
|
||||||
func (h *SSEHub) Push(userID uint, n *Notification) {
|
func (h *SSEHub) Push(userID uint, n *Notification) {
|
||||||
h.mu.RLock()
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
chs, ok := h.clients[userID]
|
chs, ok := h.clients[userID]
|
||||||
h.mu.RUnlock()
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -52,13 +54,27 @@ func (h *SSEHub) Unsubscribe(userID uint, ch chan *Notification) {
|
|||||||
chs := h.clients[userID]
|
chs := h.clients[userID]
|
||||||
for i, c := range chs {
|
for i, c := range chs {
|
||||||
if c == ch {
|
if c == ch {
|
||||||
h.clients[userID] = append(chs[:i], chs[i+1:]...)
|
chs = append(chs[:i], chs[i+1:]...)
|
||||||
|
if len(chs) == 0 {
|
||||||
|
delete(h.clients, userID)
|
||||||
|
} else {
|
||||||
|
h.clients[userID] = chs
|
||||||
|
}
|
||||||
close(c)
|
close(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sseAccountID(c *gin.Context) (uint, bool) {
|
||||||
|
accountID, ok := c.Get("accountID")
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
userID, ok := accountID.(uint)
|
||||||
|
return userID, ok && userID != 0
|
||||||
|
}
|
||||||
|
|
||||||
func (h *SSEHub) SSERequireAuth() gin.HandlerFunc {
|
func (h *SSEHub) SSERequireAuth() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
token := c.Query("token")
|
token := c.Query("token")
|
||||||
@@ -83,8 +99,11 @@ func (h *SSEHub) SSERequireAuth() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *SSEHub) SSEHandler(c *gin.Context) {
|
func (h *SSEHub) SSEHandler(c *gin.Context) {
|
||||||
accountID, _ := c.Get("accountID")
|
userID, ok := sseAccountID(c)
|
||||||
userID := accountID.(uint)
|
if !ok {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid account"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
||||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||||
@@ -120,8 +139,11 @@ func (h *SSEHub) SSEHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *SSEHub) ListHandler(c *gin.Context) {
|
func (h *SSEHub) ListHandler(c *gin.Context) {
|
||||||
accountID, _ := c.Get("accountID")
|
userID, ok := sseAccountID(c)
|
||||||
userID := accountID.(uint)
|
if !ok {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid account"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var notifications []Notification
|
var notifications []Notification
|
||||||
if err := h.db.WithContext(c.Request.Context()).
|
if err := h.db.WithContext(c.Request.Context()).
|
||||||
@@ -139,28 +161,45 @@ func (h *SSEHub) ListHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *SSEHub) MarkReadHandler(c *gin.Context) {
|
func (h *SSEHub) MarkReadHandler(c *gin.Context) {
|
||||||
accountID, _ := c.Get("accountID")
|
userID, ok := sseAccountID(c)
|
||||||
userID := accountID.(uint)
|
if !ok {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid account"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
ID *uint `json:"id"`
|
ID *uint `json:"id"`
|
||||||
}
|
}
|
||||||
c.ShouldBindJSON(&req)
|
if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
if req.ID != nil {
|
if req.ID != nil {
|
||||||
h.db.WithContext(c.Request.Context()).Model(&Notification{}).Where("id = ? AND recipient_id = ?", *req.ID, userID).Update("is_read", true)
|
err = h.db.WithContext(c.Request.Context()).Model(&Notification{}).Where("id = ? AND recipient_id = ?", *req.ID, userID).Update("is_read", true).Error
|
||||||
} else {
|
} else {
|
||||||
h.db.WithContext(c.Request.Context()).Model(&Notification{}).Where("recipient_id = ?", userID).Update("is_read", true)
|
err = h.db.WithContext(c.Request.Context()).Model(&Notification{}).Where("recipient_id = ?", userID).Update("is_read", true).Error
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
c.JSON(200, gin.H{"message": "ok"})
|
c.JSON(200, gin.H{"message": "ok"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SSEHub) UnreadCountHandler(c *gin.Context) {
|
func (h *SSEHub) UnreadCountHandler(c *gin.Context) {
|
||||||
accountID, _ := c.Get("accountID")
|
userID, ok := sseAccountID(c)
|
||||||
userID := accountID.(uint)
|
if !ok {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid account"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var count int64
|
var count int64
|
||||||
h.db.WithContext(c.Request.Context()).Model(&Notification{}).Where("recipient_id = ? AND is_read = ?", userID, false).Count(&count)
|
if err := h.db.WithContext(c.Request.Context()).Model(&Notification{}).Where("recipient_id = ? AND is_read = ?", userID, false).Count(&count).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
c.JSON(200, gin.H{"count": count})
|
c.JSON(200, gin.H{"count": count})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ services:
|
|||||||
target: api
|
target: api
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
|
CONFIG_PATH: /app/configs/config.yaml
|
||||||
JWT_SECRET: ${JWT_SECRET:-feedsystem-dev-secret-key}
|
JWT_SECRET: ${JWT_SECRET:-feedsystem-dev-secret-key}
|
||||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-feedsystem}
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-feedsystem}
|
||||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-123456}
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-123456}
|
||||||
@@ -79,7 +80,7 @@ services:
|
|||||||
rabbitmq:
|
rabbitmq:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pgrep api || exit 1"]
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz || exit 1"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
@@ -91,6 +92,7 @@ services:
|
|||||||
target: worker
|
target: worker
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
|
CONFIG_PATH: /app/configs/config.yaml
|
||||||
JWT_SECRET: ${JWT_SECRET:-feedsystem-dev-secret-key}
|
JWT_SECRET: ${JWT_SECRET:-feedsystem-dev-secret-key}
|
||||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-feedsystem}
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-feedsystem}
|
||||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-123456}
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-123456}
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ function readStored(key: string): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function writeStored(key: string, value: string) {
|
function writeStored(key: string, value: string) {
|
||||||
localStorage.setItem(key, value)
|
try { localStorage.setItem(key, value) } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeStored(key: string) {
|
function removeStored(key: string) {
|
||||||
localStorage.removeItem(key)
|
try { localStorage.removeItem(key) } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user