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" "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 { oss *ali.OSSClient job *job.Job app *app.Config } // List posts // // @Summary 作品列表 // @Tags Posts // @Produce json // @Param pagination query requests.Pagination false "分页参数" // @Param query query ListQuery false "筛选条件" // @Success 200 {object} requests.Pager{items=PostItem} "成功" // @Router /v1/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), } if query.Keyword != nil && *query.Keyword != "" { conds = append(conds, 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 // // @Summary 作品详情 // @Tags Posts // @Produce json // @Param id path int64 true "作品 ID" // @Success 200 {object} PostItem "成功" // @Router /v1/posts/:id/show [get] // @Bind post path key(id) model(id) // @Bind user local func (ctl *posts) Show(ctx fiber.Ctx, post *models.Post, user *models.User) (*PostItem, error) { log.Infof("Fetching post with ID: %d", post.ID) if post.Status != fields.PostStatusPublished { return nil, fiber.ErrNotFound } 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 // // @Summary 获取播放地址 // @Tags Posts // @Produce json // @Param id path int64 true "作品 ID" // @Success 200 {object} PlayUrl "成功" // @Router /v1/posts/:id/play [get] // @Bind post path key(id) model(id) // @Bind user local func (ctl *posts) Play(ctx fiber.Ctx, post *models.Post, user *models.User) (*PlayUrl, error) { log := log.WithField("PlayPostID", strconv.FormatInt(post.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, post.ID) if !bought || err != nil { preview = true } log.Infof("Fetching play URL for post ID: %d", post.ID) 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.Media.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 // // @Summary 我的已购作品 // @Tags Posts // @Produce json // @Param pagination query requests.Pagination false "分页参数" // @Param query query ListQuery false "筛选条件" // @Success 200 {object} requests.Pager{items=PostItem} "成功" // @Router /v1/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.Media.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 // // @Summary 购买作品 // @Tags Posts // @Produce json // @Param id path int64 true "作品 ID" // @Success 200 {object} wechat.JSAPIPayParams "成功(余额支付返回 AppId=balance)" // @Router /v1/posts/:id/buy [post] // @Bind post path key(id) model(id) // @Bind user local func (ctl *posts) Buy(ctx fiber.Ctx, post *models.Post, user *models.User) (*wechat.JSAPIPayParams, error) { bought, err := services.Users.HasBought(ctx, user.ID, post.ID) if err != nil { return nil, errors.New("查询购买失败") } if bought { return nil, errors.New("已经购买过了") } // payPrice := post.PayPrice() order, err := services.Orders.CreateFromUserPostID(ctx, user.ID, post) 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) }