package http import ( _ "embed" "strconv" "time" "quyun/app/jobs" "quyun/app/models" "quyun/app/requests" "quyun/database/conds" "quyun/database/fields" "quyun/database/schemas/public/model" "quyun/providers/ali" "quyun/providers/job" "quyun/providers/wepay" "github.com/go-pay/gopay/wechat/v3" "github.com/gofiber/fiber/v3" "github.com/pkg/errors" "github.com/samber/lo" log "github.com/sirupsen/logrus" ) type ListQuery struct { Keyword *string `query:"keyword"` } // @provider type posts struct { wepay *wepay.Client oss *ali.OSSClient job *job.Job } // List posts // // @Router /posts [get] // @Bind pagination query // @Bind query query // @Bind user local func (ctl *posts) List(ctx fiber.Ctx, pagination *requests.Pagination, query *ListQuery, user *model.Users) (*requests.Pager, error) { conds := []conds.Cond{ conds.Post_NotDeleted(), conds.Post_Status(fields.PostStatusPublished), conds.Post_Like(query.Keyword), } pager, err := models.Posts.List(ctx.Context(), pagination, conds...) if err != nil { log.WithError(err).Errorf("post list err: %v", err) return nil, err } postIds := lo.Map(pager.Items.([]model.Posts), func(item model.Posts, _ int) int64 { return item.ID }) if len(postIds) > 0 { userBoughtIds, err := models.Users.BatchCheckHasBought(ctx.Context(), user.ID, postIds) if err != nil { log.WithError(err).Errorf("BatchCheckHasBought err: %v", err) } items := lo.FilterMap(pager.Items.([]model.Posts), func(item model.Posts, _ int) (PostItem, bool) { medias, err := models.Posts.GetMediaByIds(ctx.Context(), item.HeadImages.Data) if err != nil { log.Errorf("GetMediaByIds err: %v", err) return PostItem{}, false } mediaUrls := lo.FilterMap(medias, func(item model.Medias, _ int) (string, bool) { url, err := ctl.oss.GetSignedUrl(ctx.Context(), item.Path) if err != nil { log.WithError(err).Errorf("head image GetSignedUrl err: %v", err) return "", false } return url, true }) _, bought := userBoughtIds[item.ID] return PostItem{ ID: item.ID, Title: item.Title, Description: item.Description, Price: item.Price, Discount: item.Discount, Views: item.Views, Likes: item.Likes, Tags: item.Tags.Data, HeadImages: mediaUrls, Bought: bought, }, true }) pager.Items = items } return pager, nil } type PostItem struct { ID int64 `json:"id"` Bought bool `json:"bought"` Title string `json:"title"` Description string `json:"description"` Content string `json:"content"` Price int64 `json:"price"` Discount int16 `json:"discount"` Views int64 `json:"views"` Likes int64 `json:"likes"` Tags []string `json:"tags"` HeadImages []string `json:"head_images"` } // Show // // @Router /posts/:id/show [get] // @Bind id path // @Bind user local func (ctl *posts) Show(ctx fiber.Ctx, id int64, user *model.Users) (*PostItem, error) { log.Infof("Fetching post with ID: %d", id) post, err := models.Posts.GetByID(ctx.Context(), id, conds.Post_NotDeleted(), conds.Post_Status(fields.PostStatusPublished)) if err != nil { log.WithError(err).Errorf("GetByID err: %v", err) return nil, err } bought, err := models.Users.HasBought(ctx.Context(), user.ID, post.ID) if err != nil { return nil, err } medias, err := models.Posts.GetMediaByIds(ctx.Context(), post.HeadImages.Data) if err != nil { return nil, err } mediaUrls := lo.FilterMap(medias, func(item model.Medias, _ int) (string, bool) { url, err := ctl.oss.GetSignedUrl(ctx.Context(), item.Path) if err != nil { return "", false } return url, true }) return &PostItem{ ID: post.ID, Title: post.Title, Description: post.Description, Content: post.Content, Price: post.Price, Discount: post.Discount, Views: post.Views, Likes: post.Likes, Tags: post.Tags.Data, HeadImages: mediaUrls, Bought: bought, }, nil } type PlayUrl struct { Url string `json:"url"` } // Play // // @Router /posts/:id/play [get] // @Bind id path // @Bind user local func (ctl *posts) Play(ctx fiber.Ctx, id int64, user *model.Users) (*PlayUrl, error) { log := log.WithField("PlayPostID", strconv.FormatInt(id, 10)) // return &PlayUrl{ // Url: "https://github.com/mediaelement/mediaelement-files/raw/refs/heads/master/big_buck_bunny.mp4", // }, nil log.Infof("Fetching play URL for post ID: %d", id) post, err := models.Posts.GetByID(ctx.Context(), id) if err != nil { log.WithError(err).Errorf("GetByID err: %v", err) return nil, err } go models.Posts.IncrViewCount(ctx.Context(), post.ID) preview := false bought, err := models.Users.HasBought(ctx.Context(), user.ID, post.ID) if !bought || err != nil { preview = true } for _, asset := range post.Assets.Data { if asset.Type == "video/mp4" && asset.Metas != nil && asset.Metas.Short == preview { media, err := models.Medias.GetByID(ctx.Context(), asset.Media) if err != nil { log.WithError(err).Errorf("medias GetByID err: %v", err) return nil, err } url, err := ctl.oss.GetSignedUrl(ctx.Context(), media.Path) if err != nil { log.WithError(err).Errorf("media GetSignedUrl err: %v", err) return nil, err } return &PlayUrl{Url: url}, nil } } return nil, errors.New("视频不存在") } // Mine posts // // @Router /posts/mine [get] // @Bind pagination query // @Bind query query // @Bind user local func (ctl *posts) Mine(ctx fiber.Ctx, pagination *requests.Pagination, query *ListQuery, user *model.Users) (*requests.Pager, error) { log.Infof("Fetching posts for user with pagination: %+v and keyword: %v", pagination, query.Keyword) conds := []conds.Cond{ conds.Post_NotDeleted(), conds.Post_Status(fields.PostStatusPublished), conds.Post_Like(query.Keyword), } pager, err := models.Users.PostList(ctx.Context(), user.ID, pagination, conds...) if err != nil { log.WithError(err).Errorf("post list err: %v", err) return nil, err } postIds := lo.Map(pager.Items.([]model.Posts), func(item model.Posts, _ int) int64 { return item.ID }) if len(postIds) > 0 { items := lo.FilterMap(pager.Items.([]model.Posts), func(item model.Posts, _ int) (PostItem, bool) { medias, err := models.Posts.GetMediaByIds(ctx.Context(), item.HeadImages.Data) if err != nil { log.Errorf("GetMediaByIds err: %v", err) return PostItem{}, false } mediaUrls := lo.FilterMap(medias, func(item model.Medias, _ int) (string, bool) { url, err := ctl.oss.GetSignedUrl(ctx.Context(), item.Path) if err != nil { log.WithError(err).Errorf("head image GetSignedUrl err: %v", err) return "", false } return url, true }) return PostItem{ ID: item.ID, Title: item.Title, Description: item.Description, Price: item.Price, Discount: item.Discount, Views: item.Views, Likes: item.Likes, Tags: item.Tags.Data, HeadImages: mediaUrls, }, true }) pager.Items = items } return pager, nil } // Buy // // @Router /posts/:id/buy [post] // @Bind id path // @Bind user local func (ctl *posts) Buy(ctx fiber.Ctx, id int64, user *model.Users) (*wechat.JSAPIPayParams, error) { post, err := models.Posts.GetByID(ctx.Context(), id) if err != nil { return nil, errors.Wrapf(err, " failed to get post: %d", id) } // create order order, err := models.Orders.Create(ctx.Context(), user.ID, post.ID) if err != nil { return nil, errors.Wrap(err, "订单创建失败") } payPrice := post.Price * int64(post.Discount) / 100 if user.Balance >= payPrice { err = models.Users.SetBalance(ctx.Context(), user.ID, user.Balance-payPrice) if err != nil { return nil, errors.Wrap(err, "余额支付失败") } if err := ctl.job.Add(&jobs.BalancePayNotify{OrderNo: order.OrderNo}); err != nil { log.Errorf("add job error:%v", err) return nil, errors.Wrap(err, "Failed to add job") } return &wechat.JSAPIPayParams{ AppId: "balance", }, nil } prePayResp, err := ctl.wepay.V3TransactionJsapi(ctx.Context(), func(bm *wepay.BodyMap) { bm. Expire(30 * time.Minute). Description(post.Title). OutTradeNo(order.OrderNo). Payer(user.OpenID). CNYAmount(payPrice) }) if err != nil { log.Errorf("wepay.V3TransactionJsapi err: %v", err) return nil, errors.Wrap(err, "微信支付失败") } return prePayResp.PaySignOfJSAPI() }