feat: user buy media

This commit is contained in:
Rogee
2024-12-10 11:21:55 +08:00
parent 21840c3adf
commit e3ef31037c
17 changed files with 201 additions and 132 deletions

View File

@@ -2,7 +2,6 @@ package medias
import (
"backend/pkg/consts"
"backend/pkg/errorx"
"backend/pkg/pg"
"backend/providers/jwt"
@@ -22,13 +21,13 @@ func (c *Controller) List(ctx fiber.Ctx) error {
filter := ListFilter{}
if err := ctx.Bind().Body(&filter); err != nil {
log.WithError(err).Error("parse body failed")
return ctx.Status(fiber.StatusBadRequest).JSON(errorx.RequestParseError)
return err
}
claim := ctx.Locals(consts.CtxKeyClaim).(*jwt.Claims)
items, err := c.svc.List(ctx.Context(), claim.TenantID, claim.UserID, &filter)
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.InternalError)
return err
}
return ctx.JSON(items)
@@ -43,14 +42,14 @@ func (c *Controller) Show(ctx fiber.Ctx) error {
model, err := c.svc.GetMediaByHash(ctx.Context(), claim.TenantID, hash)
if err != nil {
log.WithField("action", "medias.Show").WithError(err).Error("GetMediaByHash")
return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.InternalError)
return err
}
resource := c.svc.ModelToListItem(ctx.Context(), model)
resource.Bought, err = c.svc.HasUserBought(ctx.Context(), claim.TenantID, claim.UserID, model.ID)
if err != nil {
log.WithField("action", "medias.Show").WithError(err).Error("HasUserBought")
return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.InternalError)
return err
}
return ctx.JSON(resource)
@@ -60,7 +59,7 @@ func (c *Controller) Show(ctx fiber.Ctx) error {
func (c *Controller) MediaIndex(ctx fiber.Ctx) error {
mediaType, err := pg.ParseMediaType(ctx.Params("type"))
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.RequestParseError)
return err
}
hash := ctx.Params("hash")
@@ -70,19 +69,19 @@ func (c *Controller) MediaIndex(ctx fiber.Ctx) error {
model, err := c.svc.GetMediaByHash(ctx.Context(), claim.TenantID, hash)
if err != nil {
log.WithField("action", "medias.MediaIndex").WithError(err).Error("GetMediaByHash")
return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.InternalError)
return err
}
bought, err := c.svc.HasUserBought(ctx.Context(), claim.TenantID, claim.UserID, model.ID)
if err != nil {
log.WithField("action", "medias.MediaIndex").WithError(err).Error("HasUserBought")
return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.InternalError)
return err
}
playlist, err := c.svc.GetM3U8(ctx.Context(), claim.TenantID, mediaType, model.Hash, bought)
if err != nil {
log.WithField("action", "medias.MediaIndex").WithError(err).Error("GetMediaPlaylist")
return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.InternalError)
return err
}
return ctx.SendString(playlist.String())
@@ -91,14 +90,14 @@ func (c *Controller) MediaIndex(ctx fiber.Ctx) error {
func (c *Controller) MediaSegment(ctx fiber.Ctx) error {
mediaType, err := pg.ParseMediaType(ctx.Params("type"))
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.RequestParseError)
return err
}
segment := ctx.Params("segment")
segments, err := c.hashIds.DecodeInt64WithError(segment)
if err != nil {
log.WithField("action", "medias.MediaSegment").WithError(err).Error("DecodeInt64WithError")
return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.RequestParseError)
return err
}
hash := ctx.Params("hash")
@@ -108,9 +107,29 @@ func (c *Controller) MediaSegment(ctx fiber.Ctx) error {
model, err := c.svc.GetMediaByHash(ctx.Context(), claim.TenantID, hash)
if err != nil {
log.WithField("action", "medias.MediaSegment").WithError(err).Error("GetMediaByHash")
return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.InternalError)
return err
}
filepath := c.svc.GetSegmentPath(ctx.Context(), mediaType, model.Hash, segments[0])
return ctx.SendFile(filepath)
}
// Checkout
func (c *Controller) Checkout(ctx fiber.Ctx) error {
hash := ctx.Params("hash")
claim := fiber.Locals[*jwt.Claims](ctx, consts.CtxKeyClaim)
log.Debug(claim)
model, err := c.svc.GetMediaByHash(ctx.Context(), claim.TenantID, hash)
if err != nil {
log.WithField("action", "medias.MediaSegment").WithError(err).Error("GetMediaByHash")
return err
}
if err := c.svc.Checkout(ctx.Context(), claim.TenantID, claim.UserID, model.ID); err != nil {
log.WithField("action", "medias.MediaSegment").WithError(err).Error("Checkout")
return err
}
return ctx.JSON(nil)
}

View File

@@ -29,4 +29,5 @@ func (r *Router) Register(router fiber.Router) {
group.Get(":hash", r.controller.Show)
group.Get(":hash/:type<regex([video|audio])>", r.controller.MediaIndex)
group.Get(":hash/:type<regex([video|audio])>/:segment.ts", r.controller.MediaSegment)
group.Get(":hash/checkout", r.controller.Checkout)
}

View File

@@ -11,6 +11,7 @@ import (
"backend/database/models/qvyun/public/model"
"backend/database/models/qvyun/public/table"
"backend/pkg/errorx"
"backend/pkg/media_store"
"backend/pkg/path"
"backend/pkg/pg"
@@ -314,3 +315,94 @@ func (svc *Service) GetM3U8(ctx context.Context, tenantId int64, types pg.MediaT
func (svc *Service) GetSegmentPath(ctx context.Context, t pg.MediaType, hash string, segment int64) string {
return filepath.Join(svc.storageConfig.Path, hash, t.String(), fmt.Sprintf("%d.ts", segment))
}
func (svc *Service) Checkout(ctx context.Context, tenantId, userId, mediaId int64) error {
log := svc.log.WithField("method", "Checkout")
bought, err := svc.HasUserBought(ctx, tenantId, userId, mediaId)
if err != nil {
return errors.Wrap(err, "check user bought")
}
if bought {
return nil
}
media, err := svc.GetMediaByID(ctx, tenantId, userId, mediaId)
if err != nil {
return errors.Wrap(err, "get media")
}
userBalance, err := svc.GetUserBalance(ctx, tenantId, userId)
if err != nil {
return errors.Wrap(err, "get user balance")
}
if userBalance < media.Price {
return errorx.UserBalanceNotEnough
}
tx, err := svc.db.Begin()
if err != nil {
return errors.Wrap(err, "begin transaction")
}
defer tx.Rollback()
tbl := table.UserMedias
stmt := tbl.
INSERT(tbl.TenantID, tbl.UserID, tbl.MediaID, tbl.Price).
VALUES(Int(tenantId), Int(userId), Int(mediaId), Int(media.Price))
log.Debug(stmt.DebugSql())
if _, err := stmt.ExecContext(ctx, tx); err != nil {
return errors.Wrap(err, "insert user media")
}
// update user balance
tblUserTenants := table.UsersTenants
stmtUserTenants := tblUserTenants.
UPDATE().
SET(
tblUserTenants.Balance.SET(
tblUserTenants.Balance.SUB(Int(media.Price)),
),
).
WHERE(
tblUserTenants.TenantID.EQ(Int(tenantId)).AND(
tblUserTenants.UserID.EQ(Int(userId)),
),
)
log.Debug(stmtUserTenants.DebugSql())
if _, err := stmtUserTenants.ExecContext(ctx, tx); err != nil {
return errors.Wrap(err, "update user balance")
}
if err := tx.Commit(); err != nil {
return errors.Wrap(err, "commit transaction")
}
return nil
}
// GetUserBalance
func (svc *Service) GetUserBalance(ctx context.Context, tenantId, userId int64) (int64, error) {
log := svc.log.WithField("method", "GetUserBalance")
tbl := table.UsersTenants
stmt := tbl.SELECT(tbl.Balance.AS("balance")).WHERE(
tbl.TenantID.EQ(Int(tenantId)).AND(
tbl.UserID.EQ(Int(userId)),
),
)
log.Debug(stmt.DebugSql())
var result struct {
Balance int64
}
if err := stmt.QueryContext(ctx, svc.db, &result); err != nil {
return 0, errors.Wrap(err, "query user balance")
}
return result.Balance, nil
}

View File

@@ -11,6 +11,7 @@ import (
"backend/providers/postgres"
"backend/providers/storage"
log "github.com/sirupsen/logrus"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/suite"
"go.uber.org/dig"
@@ -27,6 +28,7 @@ type ServiceTestSuite struct {
}
func Test_DiscoverMedias(t *testing.T) {
log.SetLevel(log.DebugLevel)
providers := testx.Default(
postgres.DefaultProvider(),
storage.DefaultProvider(),
@@ -66,3 +68,18 @@ func (t *ServiceTestSuite) Test_getMediaByHash() {
So(ext, ShouldEqual, "ts")
})
}
func (t *ServiceTestSuite) Test_GetUserBalance() {
Convey("Test_GetUserBalance", t.T(), func() {
balance, err := t.Svc.GetUserBalance(context.Background(), 1, 1)
So(err, ShouldBeNil)
t.T().Logf("balance: %+v", balance)
})
}
func (t *ServiceTestSuite) Test_Checkout() {
Convey("Test_Checkout", t.T(), func() {
err := t.Svc.Checkout(context.TODO(), 1, 1, 1)
So(err, ShouldBeNil)
})
}