Files
quyun/backend/app/http/posts.go
Rogee bc495544c8
Some checks failed
build quyun / Build (push) Failing after 12m45s
feat: update go module, add recharge wechat
2025-12-04 11:47:37 +08:00

355 lines
9.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package http
import (
_ "embed"
"strconv"
"time"
"quyun/app/jobs"
"quyun/app/model"
"quyun/app/requests"
"quyun/database/fields"
"quyun/providers/ali"
"quyun/providers/app"
"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
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 *model.Users,
) (*requests.Pager, error) {
conds := []model.Cond{
model.PostsModel().CondNotDeleted(),
model.PostsModel().CondStatus(fields.PostStatusPublished),
model.PostsModel().CondLike(query.Keyword),
}
pager, err := model.PostsModel().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 := user.BatchCheckHasBought(ctx.Context(), 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 := model.PostsModel().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,
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 *model.Users) (*PostItem, error) {
log.Infof("Fetching post with ID: %d", id)
post, err := model.PostsModel().
GetByID(ctx.Context(), id, model.PostsModel().CondNotDeleted(), model.PostsModel().CondStatus(fields.PostStatusPublished))
if err != nil {
log.WithError(err).Errorf("GetByID err: %v", err)
return nil, err
}
bought, err := user.HasBought(ctx.Context(), post.ID)
if err != nil {
return nil, err
}
medias, err := model.PostsModel().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,
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 *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
preview := false
bought, err := user.HasBought(ctx.Context(), id)
if !bought || err != nil {
preview = true
}
log.Infof("Fetching play URL for post ID: %d", id)
post, err := model.PostsModel().GetByID(ctx.Context(), id)
if err != nil {
log.WithError(err).Errorf("GetByID err: %v", err)
return nil, err
}
go post.IncrViewCount(ctx.Context())
for _, asset := range post.Assets.Data {
if asset.Type == "video/mp4" && asset.Metas != nil && asset.Metas.Short == preview {
media, err := model.MediasModel().GetByID(ctx.Context(), 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.Context(),
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 *model.Users,
) (*requests.Pager, error) {
log.Infof("Fetching posts for user with pagination: %+v and keyword: %v", pagination, query.Keyword)
conds := []model.Cond{
model.PostsModel().CondNotDeleted(),
model.PostsModel().CondStatus(fields.PostStatusPublished),
model.PostsModel().CondLike(query.Keyword),
}
pager, err := model.UsersModel().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 := model.PostsModel().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,
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 *model.Users) (*wechat.JSAPIPayParams, error) {
bought, err := user.HasBought(ctx.Context(), id)
if err != nil {
return nil, errors.New("查询购买失败")
}
if bought {
return nil, errors.New("已经购买过了")
}
post, err := model.PostsModel().GetByID(ctx.Context(), id)
if err != nil {
return nil, errors.Wrapf(err, " failed to get post: %d", id)
}
// payPrice := post.PayPrice()
order, err := model.OrdersModel().CreateFromUserPostID(ctx.Context(), user.ID, post.ID)
if err != nil {
return nil, errors.Wrap(err, "订单创建失败")
}
if user.Balance >= post.PayPrice() {
if err := order.SetMeta(ctx.Context(), 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)
payPrice := post.PayPrice() - user.Balance
if err := order.SetMeta(ctx.Context(), func(om fields.OrderMeta) fields.OrderMeta {
om.CostBalance = user.Balance
return om
}); err != nil {
return nil, errors.Wrap(err, "订单创建失败")
}
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()
}