package http import ( _ "embed" "strconv" "time" "quyun/v2/app/jobs" "quyun/v2/app/requests" "quyun/v2/app/services" "quyun/v2/database" "quyun/v2/database/models" "quyun/v2/pkg/fields" "quyun/v2/providers/ali" "quyun/v2/providers/app" "quyun/v2/providers/job" "quyun/v2/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" "go.ipao.vip/gen" ) type ListQuery struct { Keyword *string `query:"keyword"` } // @provider type posts struct { wepay *wepay.Client oss *ali.OSSClient job *job.Job app *app.Config } // 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 *models.User, ) (*requests.Pager, error) { tbl, _ := models.PostQuery.QueryContext(ctx) conds := []gen.Condition{ tbl.Status.Eq(fields.PostStatusPublished), tbl.Title.Like(database.WrapLike(*query.Keyword)), } pager, err := services.Posts.List(ctx, pagination, conds...) if err != nil { log.WithError(err).Errorf("post list err: %v", err) return nil, err } postIds := lo.Map(pager.Items.([]models.Post), func(item models.Post, _ int) int64 { return item.ID }) if len(postIds) > 0 { userBoughtIds, err := services.Users.BatchCheckHasBought(ctx, user.ID, postIds) if err != nil { log.WithError(err).Errorf("BatchCheckHasBought err: %v", err) } items := lo.FilterMap(pager.Items.([]models.Post), func(item models.Post, _ int) (PostItem, bool) { medias, err := services.Posts.GetMediasByIds(ctx, item.HeadImages.Data()) if err != nil { log.Errorf("GetMediaByIds err: %v", err) return PostItem{}, false } mediaUrls := lo.FilterMap(medias, func(item *models.Media, _ int) (string, bool) { url, err := ctl.oss.GetSignedUrl(ctx, 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, RechargeWechat: ctl.app.RechargeWechat, }, 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"` RechargeWechat string `json:"recharge_wechat,omitempty"` } // Show // // @Router /posts/:id/show [get] // @Bind id path // @Bind user local func (ctl *posts) Show(ctx fiber.Ctx, id int64, user *models.User) (*PostItem, error) { log.Infof("Fetching post with ID: %d", id) post, err := services.Posts.FindByID( ctx, id, models.PostQuery.Status.Eq(fields.PostStatusPublished), ) if err != nil { log.WithError(err).Errorf("GetByID err: %v", err) return nil, err } bought, err := services.Users.HasBought(ctx, user.ID, post.ID) if err != nil { return nil, err } medias, err := services.Posts.GetMediasByIds(ctx, post.HeadImages.Data()) if err != nil { return nil, err } mediaUrls := lo.FilterMap(medias, func(item *models.Media, _ int) (string, bool) { url, err := ctl.oss.GetSignedUrl(ctx, 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, RechargeWechat: ctl.app.RechargeWechat, }, 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 *models.User) (*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 preview := false bought, err := services.Users.HasBought(ctx, user.ID, id) if !bought || err != nil { preview = true } log.Infof("Fetching play URL for post ID: %d", id) post, err := services.Posts.FindByID(ctx, id) if err != nil { log.WithError(err).Errorf("GetByID err: %v", err) return nil, err } go services.Posts.IncrViewCount(ctx, post.ID) for _, asset := range post.Assets.Data() { if asset.Type == "video/mp4" && asset.Metas != nil && asset.Metas.Short == preview { media, err := services.Medias.FindByID(ctx, asset.Media) if err != nil { log.WithError(err).Errorf("medias GetByID err: %v", err) return nil, err } duration := 2*asset.Metas.Duration + 30 if asset.Metas.Duration == 0 { duration = 60 * 5 } url, err := ctl.oss.GetSignedUrl( ctx, media.Path, ali.WithExpire(time.Second*time.Duration(duration)), ) 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 *models.User, ) (*requests.Pager, error) { log.Infof("Fetching posts for user with pagination: %+v and keyword: %v", pagination, query.Keyword) conds := []gen.Condition{ models.PostQuery.Status.Eq(fields.PostStatusPublished), models.PostQuery.Title.Like(database.WrapLike(*query.Keyword)), } pager, err := services.Users.PostList(ctx, user.ID, pagination, conds...) if err != nil { log.WithError(err).Errorf("post list err: %v", err) return nil, err } postIds := lo.Map(pager.Items.([]*models.Post), func(item *models.Post, _ int) int64 { return item.ID }) if len(postIds) > 0 { items := lo.FilterMap(pager.Items.([]*models.Post), func(item *models.Post, _ int) (PostItem, bool) { medias, err := services.Medias.GetByIds(ctx, item.HeadImages.Data()) if err != nil { log.Errorf("GetMediaByIds err: %v", err) return PostItem{}, false } mediaUrls := lo.FilterMap(medias, func(item *models.Media, _ int) (string, bool) { url, err := ctl.oss.GetSignedUrl(ctx, 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, RechargeWechat: ctl.app.RechargeWechat, }, 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 *models.User) (*wechat.JSAPIPayParams, error) { bought, err := services.Users.HasBought(ctx, user.ID, id) if err != nil { return nil, errors.New("查询购买失败") } if bought { return nil, errors.New("已经购买过了") } post, err := services.Posts.FindByID(ctx, id) if err != nil { return nil, errors.Wrapf(err, " failed to get post: %d", id) } // payPrice := post.PayPrice() order, err := services.Orders.CreateFromUserPostID(ctx, user.ID, post.ID) if err != nil { return nil, errors.Wrap(err, "订单创建失败") } if user.Balance >= post.PayPrice() { if err := services.Orders.SetMeta(ctx, order.ID, func(om fields.OrderMeta) fields.OrderMeta { om.CostBalance = post.PayPrice() return om }); 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 } return nil, errors.Errorf("账户余额不足, 当前余额:%0.2f, 请联系管理员购买或充值", float64(user.Balance)/100) }