diff --git a/.gitignore b/.gitignore index aa1ec1e..5b9334f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.tgz +.gocache diff --git a/backend/app/events/publishers/user_register.go b/backend/app/events/publishers/user_register.go index 32fc555..8314cdf 100644 --- a/backend/app/events/publishers/user_register.go +++ b/backend/app/events/publishers/user_register.go @@ -11,7 +11,6 @@ import ( var _ contracts.EventPublisher = (*UserRegister)(nil) type UserRegister struct { - event.DefaultChannel ID int64 `json:"id"` } diff --git a/backend_v1/app/http/admin/auth.go b/backend_v1/app/http/admin/auth.go new file mode 100644 index 0000000..f226878 --- /dev/null +++ b/backend_v1/app/http/admin/auth.go @@ -0,0 +1,40 @@ +package admin + +import ( + "quyun/v2/providers/jwt" + + "github.com/gofiber/fiber/v3" +) + +// @provider +type auth struct { + jwt *jwt.JWT +} + +type AuthBody struct { + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` +} + +type TokenResponse struct { + Token string `json:"token"` +} + +// Login +// +// @Router /admin/auth [post] +// @Bind body body +func (ctl *auth) Login(ctx fiber.Ctx, body *AuthBody) (*TokenResponse, error) { + if body.Username == "pl.yang" && body.Password == "Xixi@0202" { + claim := ctl.jwt.CreateClaims(jwt.BaseClaims{ + UserID: -20140202, + }) + + token, err := ctl.jwt.CreateToken(claim) + if err != nil { + return nil, err + } + return &TokenResponse{Token: token}, nil + } + return nil, fiber.ErrUnauthorized +} diff --git a/backend_v1/app/http/admin/medias.go b/backend_v1/app/http/admin/medias.go new file mode 100644 index 0000000..5cdad8b --- /dev/null +++ b/backend_v1/app/http/admin/medias.go @@ -0,0 +1,63 @@ +package admin + +import ( + "quyun/v2/app/requests" + "quyun/v2/app/services" + "quyun/v2/database" + "quyun/v2/database/models" + "quyun/v2/providers/ali" + + "github.com/gofiber/fiber/v3" +) + +// @provider +type medias struct { + oss *ali.OSSClient +} + +// List medias +// +// @Router /admin/medias [get] +// @Bind pagination query +// @Bind query query +func (ctl *medias) List(ctx fiber.Ctx, pagination *requests.Pagination, query *ListQuery) (*requests.Pager, error) { + return services.Medias.List(ctx, pagination, models.MediaQuery.Name.Like(database.WrapLike(*query.Keyword))) +} + +// Show media +// +// @Router /admin/medias/:id [get] +// @Bind id path +func (ctl *medias) Show(ctx fiber.Ctx, id int64) error { + media, err := services.Medias.FindByID(ctx, id) + if err != nil { + return ctx.SendString("Media not found") + } + + url, err := ctl.oss.GetSignedUrl(ctx, media.Path) + if err != nil { + return err + } + + return ctx.Redirect().To(url) +} + +// Delete +// +// @Router /admin/medias/:id [delete] +// @Bind id path +func (ctl *medias) Delete(ctx fiber.Ctx, id int64) error { + media, err := services.Medias.FindByID(ctx, id) + if err != nil { + return ctx.SendString("Media not found") + } + + if err := ctl.oss.Delete(ctx, media.Path); err != nil { + return err + } + + if _, err := media.Delete(ctx); err != nil { + return err + } + return ctx.SendStatus(fiber.StatusNoContent) +} diff --git a/backend_v1/app/http/admin/orders.go b/backend_v1/app/http/admin/orders.go new file mode 100644 index 0000000..46318e0 --- /dev/null +++ b/backend_v1/app/http/admin/orders.go @@ -0,0 +1,109 @@ +package admin + +import ( + "fmt" + + "quyun/v2/app/requests" + "quyun/v2/app/services" + "quyun/v2/database/models" + "quyun/v2/pkg/fields" + "quyun/v2/providers/wepay" + + "github.com/gofiber/fiber/v3" + "github.com/pkg/errors" + "go.ipao.vip/gen" +) + +type OrderListQuery struct { + OrderNumber *string `query:"order_number"` + UserID *int64 `query:"user_id"` +} + +// @provider +type orders struct { + wepay *wepay.Client +} + +// List users +// +// @Router /admin/orders [get] +// @Bind pagination query +// @Bind query query +func (ctl *orders) List( + ctx fiber.Ctx, + pagination *requests.Pagination, + query *OrderListQuery, +) (*requests.Pager, error) { + conds := []gen.Condition{} + if query.OrderNumber != nil { + conds = append(conds, models.OrderQuery.OrderNo.Eq(*query.OrderNumber)) + } + + if query.UserID != nil { + conds = append(conds, models.OrderQuery.UserID.Eq(*query.UserID)) + } + + return services.Orders.List(ctx, pagination, conds...) +} + +// Refund +// @Router /admin/orders/:id/refund [post] +// @Bind id path +func (ctl *orders) Refund(ctx fiber.Ctx, id int64) error { + order, err := services.Orders.FindByID(ctx, id) + if err != nil { + return err + } + + user, err := services.Users.FindByID(ctx, order.UserID) + if err != nil { + return err + } + + post, err := services.Posts.FindByID(ctx, order.PostID) + if err != nil { + return err + } + + if order.PaymentMethod == "balance" { + if err := services.Users.AddBalance(ctx, user.ID, order.Meta.Data().CostBalance); err != nil { + return errors.Wrap(err, "add balance failed") + } + + if err := services.Users.RevokeUserPosts(ctx, user.ID, order.PostID); err != nil { + return errors.Wrap(err, "revoke posts failed") + } + + order.Status = fields.OrderStatusRefundSuccess + if _, err := order.Update(ctx); err != nil { + return errors.Wrap(err, "update order failed") + } + + return nil + } + + refundTotal := order.Price*int64(order.Discount)/100 - order.Meta.Data().CostBalance + resp, err := ctl.wepay.Refund(ctx, func(bm *wepay.BodyMap) { + bm. + OutRefundNo(order.OrderNo). + OutTradeNo(order.OrderNo). + TransactionID(order.TransactionID). + CNYRefundAmount(refundTotal, refundTotal). + RefundReason(fmt.Sprintf("%s 退款", post.Title)) + }) + if err != nil { + return err + } + + meta := order.Meta.Data() + meta.RefundResp = resp + order.Meta = meta.JsonType() + order.RefundTransactionID = resp.RefundId + order.Status = fields.OrderStatusRefundProcessing + + if _, err := order.Update(ctx); err != nil { + return err + } + + return nil +} diff --git a/backend_v1/app/http/admin/posts.go b/backend_v1/app/http/admin/posts.go new file mode 100644 index 0000000..4b47e4c --- /dev/null +++ b/backend_v1/app/http/admin/posts.go @@ -0,0 +1,215 @@ +package admin + +import ( + "quyun/v2/app/requests" + "quyun/v2/app/services" + "quyun/v2/database/models" + "quyun/v2/pkg/fields" + + "github.com/gofiber/fiber/v3" + "github.com/samber/lo" + "go.ipao.vip/gen" + "go.ipao.vip/gen/types" +) + +type ListQuery struct { + Keyword *string `query:"keyword"` +} + +// @provider +type posts struct{} + +// List posts +// +// @Router /admin/posts [get] +// @Bind pagination query +// @Bind query query +func (ctl *posts) List(ctx fiber.Ctx, pagination *requests.Pagination, query *ListQuery) (*requests.Pager, error) { + conds := []gen.Condition{ + models.PostQuery.Title.Like(*query.Keyword), + } + pager, err := services.Posts.List(ctx, pagination, conds...) + if err != nil { + return nil, err + } + + postIds := lo.Map(pager.Items.([]models.Post), func(item models.Post, _ int) int64 { + return item.ID + }) + if len(postIds) > 0 { + postCntMap, err := services.Posts.BoughtStatistics(ctx, postIds) + if err != nil { + return pager, err + } + + items := lo.Map(pager.Items.([]models.Post), func(item models.Post, _ int) PostItem { + cnt := int64(0) + if v, ok := postCntMap[item.ID]; ok { + cnt = v + } + + return PostItem{Post: &item, BoughtCount: cnt} + }) + + pager.Items = items + } + return pager, err +} + +type PostForm struct { + Title string `json:"title"` + HeadImages []int64 `json:"head_images"` + Price int64 `json:"price"` + Discount int16 `json:"discount"` + Introduction string `json:"introduction"` + Medias []int64 `json:"medias"` + Status fields.PostStatus `json:"status"` + Content string `json:"content"` +} + +// Create +// +// @Router /admin/posts [post] +// @Bind form body +func (ctl *posts) Create(ctx fiber.Ctx, form *PostForm) error { + post := models.Post{ + Title: form.Title, + HeadImages: types.NewJSONType(form.HeadImages), + Price: form.Price, + Discount: form.Discount, + Description: form.Introduction, + Status: form.Status, + Content: form.Content, + Tags: types.NewJSONType([]string{}), + Assets: types.NewJSONType([]fields.MediaAsset{}), + } + + if form.Medias != nil { + medias, err := services.Medias.GetByIds(ctx, form.Medias) + if err != nil { + return err + } + assets := lo.Map(medias, func(media *models.Media, _ int) fields.MediaAsset { + return fields.MediaAsset{ + Type: media.MimeType, + Media: media.ID, + Metas: lo.ToPtr(media.Metas.Data()), + } + }) + post.Assets = types.NewJSONType(assets) + } + + if err := post.Create(ctx); err != nil { + return err + } + return nil +} + +// Update posts +// +// @Router /admin/posts/:id [put] +// @Bind id path +// @Bind form body +func (ctl *posts) Update(ctx fiber.Ctx, id int64, form *PostForm) error { + post, err := services.Posts.FindByID(ctx, id) + if err != nil { + return err + } + post.Title = form.Title + post.HeadImages = types.NewJSONType(form.HeadImages) + post.Price = form.Price + post.Discount = form.Discount + post.Description = form.Introduction + post.Status = form.Status + post.Content = form.Content + post.Tags = types.NewJSONType([]string{}) + + if form.Medias != nil { + medias, err := services.Medias.GetByIds(ctx, form.Medias) + if err != nil { + return err + } + assets := lo.Map(medias, func(media *models.Media, _ int) fields.MediaAsset { + return fields.MediaAsset{ + Type: media.MimeType, + Media: media.ID, + Metas: lo.ToPtr(media.Metas.Data()), + } + }) + post.Assets = types.NewJSONType(assets) + } + + if _, err := post.Update(ctx); err != nil { + return err + } + return nil +} + +// Delete posts +// +// @Router /admin/posts/:id [delete] +// @Bind id path +func (ctl *posts) Delete(ctx fiber.Ctx, id int64) error { + post, err := services.Posts.FindByID(ctx, id) + if err != nil { + return err + } + if post == nil { + return fiber.ErrNotFound + } + + if _, err := post.ForceDelete(ctx); err != nil { + return err + } + return nil +} + +type PostItem struct { + *models.Post + Medias []*models.Media `json:"medias"` + BoughtCount int64 `json:"bought_count"` +} + +// Show posts by id +// +// @Router /admin/posts/:id [get] +// @Bind id path +func (ctl *posts) Show(ctx fiber.Ctx, id int64) (*PostItem, error) { + post, err := services.Posts.FindByID(ctx, id) + if err != nil { + return nil, err + } + + medias, err := services.Medias.GetByIds(ctx, lo.Map(post.Assets.Data(), func(asset fields.MediaAsset, _ int) int64 { + return asset.Media + })) + if err != nil { + return nil, err + } + return &PostItem{ + Post: post, + Medias: medias, + }, nil +} + +// SendTo +// +// @Router /admin/posts/:id/send-to/:userId [post] +// @Bind id path +// @Bind userId path +func (ctl *posts) SendTo(ctx fiber.Ctx, id, userId int64) error { + post, err := services.Posts.FindByID(ctx, id) + if err != nil { + return err + } + + user, err := services.Users.FindByID(ctx, userId) + if err != nil { + return err + } + + if err := services.Posts.SendTo(ctx, post.ID, user.ID); err != nil { + return err + } + return nil +} diff --git a/backend_v1/app/http/admin/provider.gen.go b/backend_v1/app/http/admin/provider.gen.go new file mode 100755 index 0000000..3e003bb --- /dev/null +++ b/backend_v1/app/http/admin/provider.gen.go @@ -0,0 +1,116 @@ +package admin + +import ( + "quyun/v2/app/middlewares" + "quyun/v2/providers/ali" + "quyun/v2/providers/app" + "quyun/v2/providers/job" + "quyun/v2/providers/jwt" + "quyun/v2/providers/wepay" + + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/atom/opt" +) + +func Provide(opts ...opt.Option) error { + if err := container.Container.Provide(func( + jwt *jwt.JWT, + ) (*auth, error) { + obj := &auth{ + jwt: jwt, + } + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + oss *ali.OSSClient, + ) (*medias, error) { + obj := &medias{ + oss: oss, + } + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + wepay *wepay.Client, + ) (*orders, error) { + obj := &orders{ + wepay: wepay, + } + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func() (*posts, error) { + obj := &posts{} + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + auth *auth, + medias *medias, + middlewares *middlewares.Middlewares, + orders *orders, + posts *posts, + statistics *statistics, + uploads *uploads, + users *users, + ) (contracts.HttpRoute, error) { + obj := &Routes{ + auth: auth, + medias: medias, + middlewares: middlewares, + orders: orders, + posts: posts, + statistics: statistics, + uploads: uploads, + users: users, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }, atom.GroupRoutes); err != nil { + return err + } + if err := container.Container.Provide(func() (*statistics, error) { + obj := &statistics{} + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + app *app.Config, + job *job.Job, + oss *ali.OSSClient, + ) (*uploads, error) { + obj := &uploads{ + app: app, + job: job, + oss: oss, + } + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func() (*users, error) { + obj := &users{} + + return obj, nil + }); err != nil { + return err + } + return nil +} diff --git a/backend_v1/app/http/admin/routes.gen.go b/backend_v1/app/http/admin/routes.gen.go new file mode 100644 index 0000000..6e6fe67 --- /dev/null +++ b/backend_v1/app/http/admin/routes.gen.go @@ -0,0 +1,163 @@ +// Code generated by atomctl. DO NOT EDIT. + +// Package admin provides HTTP route definitions and registration +// for the quyun/v2 application. +package admin + +import ( + "quyun/v2/app/middlewares" + "quyun/v2/app/requests" + + "github.com/gofiber/fiber/v3" + log "github.com/sirupsen/logrus" + _ "go.ipao.vip/atom" + _ "go.ipao.vip/atom/contracts" + . "go.ipao.vip/atom/fen" +) + +// Routes implements the HttpRoute contract and provides route registration +// for all controllers in the admin module. +// +// @provider contracts.HttpRoute atom.GroupRoutes +type Routes struct { + log *log.Entry `inject:"false"` + middlewares *middlewares.Middlewares + // Controller instances + auth *auth + medias *medias + orders *orders + posts *posts + statistics *statistics + uploads *uploads + users *users +} + +// Prepare initializes the routes provider with logging configuration. +func (r *Routes) Prepare() error { + r.log = log.WithField("module", "routes.admin") + r.log.Info("Initializing routes module") + return nil +} + +// Name returns the unique identifier for this routes provider. +func (r *Routes) Name() string { + return "admin" +} + +// Register registers all HTTP routes with the provided fiber router. +// Each route is registered with its corresponding controller action and parameter bindings. +func (r *Routes) Register(router fiber.Router) { + // Register routes for controller: auth + r.log.Debugf("Registering route: Post /admin/auth -> auth.Login") + router.Post("/admin/auth"[len(r.Path()):], DataFunc1( + r.auth.Login, + Body[AuthBody]("body"), + )) + // Register routes for controller: medias + r.log.Debugf("Registering route: Delete /admin/medias/:id -> medias.Delete") + router.Delete("/admin/medias/:id"[len(r.Path()):], Func1( + r.medias.Delete, + PathParam[int64]("id"), + )) + r.log.Debugf("Registering route: Get /admin/medias -> medias.List") + router.Get("/admin/medias"[len(r.Path()):], DataFunc2( + r.medias.List, + Query[requests.Pagination]("pagination"), + Query[ListQuery]("query"), + )) + r.log.Debugf("Registering route: Get /admin/medias/:id -> medias.Show") + router.Get("/admin/medias/:id"[len(r.Path()):], Func1( + r.medias.Show, + PathParam[int64]("id"), + )) + // Register routes for controller: orders + r.log.Debugf("Registering route: Get /admin/orders -> orders.List") + router.Get("/admin/orders"[len(r.Path()):], DataFunc2( + r.orders.List, + Query[requests.Pagination]("pagination"), + Query[OrderListQuery]("query"), + )) + r.log.Debugf("Registering route: Post /admin/orders/:id/refund -> orders.Refund") + router.Post("/admin/orders/:id/refund"[len(r.Path()):], Func1( + r.orders.Refund, + PathParam[int64]("id"), + )) + // Register routes for controller: posts + r.log.Debugf("Registering route: Delete /admin/posts/:id -> posts.Delete") + router.Delete("/admin/posts/:id"[len(r.Path()):], Func1( + r.posts.Delete, + PathParam[int64]("id"), + )) + r.log.Debugf("Registering route: Get /admin/posts -> posts.List") + router.Get("/admin/posts"[len(r.Path()):], DataFunc2( + r.posts.List, + Query[requests.Pagination]("pagination"), + Query[ListQuery]("query"), + )) + r.log.Debugf("Registering route: Get /admin/posts/:id -> posts.Show") + router.Get("/admin/posts/:id"[len(r.Path()):], DataFunc1( + r.posts.Show, + PathParam[int64]("id"), + )) + r.log.Debugf("Registering route: Post /admin/posts -> posts.Create") + router.Post("/admin/posts"[len(r.Path()):], Func1( + r.posts.Create, + Body[PostForm]("form"), + )) + r.log.Debugf("Registering route: Post /admin/posts/:id/send-to/:userId -> posts.SendTo") + router.Post("/admin/posts/:id/send-to/:userId"[len(r.Path()):], Func2( + r.posts.SendTo, + PathParam[int64]("id"), + PathParam[int64]("userId"), + )) + r.log.Debugf("Registering route: Put /admin/posts/:id -> posts.Update") + router.Put("/admin/posts/:id"[len(r.Path()):], Func2( + r.posts.Update, + PathParam[int64]("id"), + Body[PostForm]("form"), + )) + // Register routes for controller: statistics + r.log.Debugf("Registering route: Get /admin/statistics -> statistics.statistics") + router.Get("/admin/statistics"[len(r.Path()):], DataFunc0( + r.statistics.statistics, + )) + // Register routes for controller: uploads + r.log.Debugf("Registering route: Get /admin/uploads/pre-uploaded-check/:md5.:ext -> uploads.PreUploadCheck") + router.Get("/admin/uploads/pre-uploaded-check/:md5.:ext"[len(r.Path()):], DataFunc3( + r.uploads.PreUploadCheck, + PathParam[string]("md5"), + PathParam[string]("ext"), + QueryParam[string]("mime"), + )) + r.log.Debugf("Registering route: Post /admin/uploads/post-uploaded-action -> uploads.PostUploadedAction") + router.Post("/admin/uploads/post-uploaded-action"[len(r.Path()):], Func1( + r.uploads.PostUploadedAction, + Body[PostUploadedForm]("body"), + )) + // Register routes for controller: users + r.log.Debugf("Registering route: Get /admin/users -> users.List") + router.Get("/admin/users"[len(r.Path()):], DataFunc2( + r.users.List, + Query[requests.Pagination]("pagination"), + Query[UserListQuery]("query"), + )) + r.log.Debugf("Registering route: Get /admin/users/:id -> users.Show") + router.Get("/admin/users/:id"[len(r.Path()):], DataFunc1( + r.users.Show, + PathParam[int64]("id"), + )) + r.log.Debugf("Registering route: Get /admin/users/:id/articles -> users.Articles") + router.Get("/admin/users/:id/articles"[len(r.Path()):], DataFunc2( + r.users.Articles, + PathParam[int64]("id"), + Query[requests.Pagination]("pagination"), + )) + r.log.Debugf("Registering route: Post /admin/users/:id/balance -> users.Balance") + router.Post("/admin/users/:id/balance"[len(r.Path()):], Func2( + r.users.Balance, + PathParam[int64]("id"), + Body[UserBalance]("balance"), + )) + + r.log.Info("Successfully registered all routes") +} diff --git a/backend_v1/app/http/admin/routes.manual.go b/backend_v1/app/http/admin/routes.manual.go new file mode 100644 index 0000000..003e032 --- /dev/null +++ b/backend_v1/app/http/admin/routes.manual.go @@ -0,0 +1,9 @@ +package admin + +func (r *Routes) Path() string { + return "/admin" +} + +func (r *Routes) Middlewares() []any { + return []any{} +} diff --git a/backend_v1/app/http/admin/statistics.go b/backend_v1/app/http/admin/statistics.go new file mode 100644 index 0000000..c05b441 --- /dev/null +++ b/backend_v1/app/http/admin/statistics.go @@ -0,0 +1,68 @@ +package admin + +import ( + "quyun/v2/app/services" + "quyun/v2/database/models" + "quyun/v2/pkg/fields" + + "github.com/gofiber/fiber/v3" +) + +// @provider +type statistics struct{} + +type StatisticsResponse struct { + PostDraft int64 `json:"post_draft"` + PostPublished int64 `json:"post_published"` + Media int64 `json:"media"` + Order int64 `json:"order"` + User int64 `json:"user"` + Amount int64 `json:"amount"` +} + +// dashboard statistics +// +// @Router /admin/statistics [get] +func (s *statistics) statistics(ctx fiber.Ctx) (*StatisticsResponse, error) { + statistics := &StatisticsResponse{} + + var err error + + statistics.PostDraft, err = services.Posts.Count( + ctx, + models.PostQuery.Status.Eq(fields.PostStatusDraft), + ) + if err != nil { + return nil, err + } + statistics.PostPublished, err = services.Posts.Count( + ctx, + models.PostQuery.Status.Eq(fields.PostStatusPublished), + ) + if err != nil { + return nil, err + } + + statistics.Media, err = services.Medias.Count(ctx) + if err != nil { + return nil, err + } + + // model.ExprCond(table.Orders.Status.EQ(Int(int64(fields.OrderStatusCompleted))))) + statistics.Order, err = services.Orders.Count(ctx, models.OrderQuery.Status.Eq(fields.OrderStatusCompleted)) + if err != nil { + return nil, err + } + + statistics.User, err = services.Users.Count(ctx) + if err != nil { + return nil, err + } + + statistics.Amount, err = services.Orders.SumAmount(ctx) + if err != nil { + return nil, err + } + + return statistics, nil +} diff --git a/backend_v1/app/http/admin/uploads.go b/backend_v1/app/http/admin/uploads.go new file mode 100644 index 0000000..d2b4a36 --- /dev/null +++ b/backend_v1/app/http/admin/uploads.go @@ -0,0 +1,90 @@ +package admin + +import ( + "errors" + "fmt" + "path/filepath" + + "quyun/v2/app/jobs" + "quyun/v2/app/services" + "quyun/v2/database/models" + "quyun/v2/providers/ali" + "quyun/v2/providers/app" + "quyun/v2/providers/job" + + "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss" + "github.com/gofiber/fiber/v3" + log "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +const UPLOAD_PATH = "quyun" + +// @provider +type uploads struct { + app *app.Config + oss *ali.OSSClient + job *job.Job +} + +type PreCheckResp struct { + Exists bool `json:"exists"` + PreSign *oss.PresignResult `json:"pre_sign"` +} + +// PreUploadCheck +// +// @Router /admin/uploads/pre-uploaded-check/:md5.:ext [get] +// @Bind md5 path +// @Bind ext path +// @Bind mime query +func (up *uploads) PreUploadCheck(ctx fiber.Ctx, md5, ext, mime string) (*PreCheckResp, error) { + _, err := services.Medias.GetByHash(ctx, md5) + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + preSign, err := up.oss.PreSignUpload(ctx, fmt.Sprintf("%s.%s", md5, ext), mime) + if err != nil { + return nil, err + } + + return &PreCheckResp{Exists: false, PreSign: preSign}, nil + } + + return &PreCheckResp{Exists: true}, nil +} + +type PostUploadedForm struct { + OriginalName string `json:"originalName"` + Md5 string `json:"md5"` + MimeType string `json:"mimeType"` + Size int64 `json:"size"` +} + +// PostUploadedAction +// +// @Router /admin/uploads/post-uploaded-action [post] +// @Bind body body +func (up *uploads) PostUploadedAction(ctx fiber.Ctx, body *PostUploadedForm) error { + m, err := services.Medias.GetByHash(ctx, body.Md5) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + + m = &models.Media{ + Name: body.OriginalName, + MimeType: body.MimeType, + Size: body.Size, + Hash: body.Md5, + Path: filepath.Join(UPLOAD_PATH, body.Md5+filepath.Ext(body.OriginalName)), + } + if err := m.Create(ctx); err != nil { + return err + } + + if m.MimeType == "video/mp4" { + if err := up.job.Add(&jobs.DownloadFromAliOSS{MediaHash: m.Hash}); err != nil { + log.WithError(err).WithField("media", m).Errorf("add job failed") + } + } + + return nil +} diff --git a/backend_v1/app/http/admin/users.go b/backend_v1/app/http/admin/users.go new file mode 100644 index 0000000..c4daa67 --- /dev/null +++ b/backend_v1/app/http/admin/users.go @@ -0,0 +1,64 @@ +package admin + +import ( + "quyun/v2/app/requests" + "quyun/v2/app/services" + "quyun/v2/database" + "quyun/v2/database/models" + + "github.com/gofiber/fiber/v3" + "go.ipao.vip/gen" +) + +type UserListQuery struct { + Keyword *string `query:"keyword"` +} + +// @provider +type users struct{} + +// List users +// +// @Router /admin/users [get] +// @Bind pagination query +// @Bind query query +func (ctl *users) List(ctx fiber.Ctx, pagination *requests.Pagination, query *UserListQuery) (*requests.Pager, error) { + conds := []gen.Condition{ + models.UserQuery.Username.Like(database.WrapLike(*query.Keyword)), + } + return services.Users.List(ctx, pagination, conds...) +} + +// Show user +// +// @Router /admin/users/:id [get] +// @Bind id path +func (ctl *users) Show(ctx fiber.Ctx, id int64) (*models.User, error) { + return services.Users.FindByID(ctx, id) +} + +// Articles show user bought articles +// +// @Router /admin/users/:id/articles [get] +// @Bind id path +// @Bind pagination query +func (ctl *users) Articles(ctx fiber.Ctx, id int64, pagination *requests.Pagination) (*requests.Pager, error) { + return services.Posts.Bought(ctx, id, pagination) +} + +type UserBalance struct { + Balance int64 `json:"balance"` +} + +// Balance +// +// @Router /admin/users/:id/balance [post] +// @Bind id path +// @Bind balance body +func (ctl *users) Balance(ctx fiber.Ctx, id int64, balance *UserBalance) error { + user, err := services.Users.FindByID(ctx, id) + if err != nil { + return err + } + return services.Users.AddBalance(ctx, user.ID, balance.Balance) +} diff --git a/backend_v1/app/http/posts.go b/backend_v1/app/http/posts.go new file mode 100644 index 0000000..06dbf82 --- /dev/null +++ b/backend_v1/app/http/posts.go @@ -0,0 +1,337 @@ +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) +} diff --git a/backend_v1/app/http/provider.gen.go b/backend_v1/app/http/provider.gen.go new file mode 100755 index 0000000..9ea36c9 --- /dev/null +++ b/backend_v1/app/http/provider.gen.go @@ -0,0 +1,60 @@ +package http + +import ( + "quyun/v2/app/middlewares" + "quyun/v2/providers/ali" + "quyun/v2/providers/app" + "quyun/v2/providers/job" + "quyun/v2/providers/wepay" + + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/atom/contracts" + "go.ipao.vip/atom/opt" +) + +func Provide(opts ...opt.Option) error { + if err := container.Container.Provide(func( + app *app.Config, + job *job.Job, + oss *ali.OSSClient, + wepay *wepay.Client, + ) (*posts, error) { + obj := &posts{ + app: app, + job: job, + oss: oss, + wepay: wepay, + } + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + middlewares *middlewares.Middlewares, + posts *posts, + users *users, + ) (contracts.HttpRoute, error) { + obj := &Routes{ + middlewares: middlewares, + posts: posts, + users: users, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }, atom.GroupRoutes); err != nil { + return err + } + if err := container.Container.Provide(func() (*users, error) { + obj := &users{} + + return obj, nil + }); err != nil { + return err + } + return nil +} diff --git a/backend_v1/app/http/provider.manual.go b/backend_v1/app/http/provider.manual.go new file mode 100755 index 0000000..e0d6644 --- /dev/null +++ b/backend_v1/app/http/provider.manual.go @@ -0,0 +1,14 @@ +package http + +import ( + "quyun/v2/app/http/admin" + + "go.ipao.vip/atom/container" +) + +func Providers() []container.ProviderContainer { + return []container.ProviderContainer{ + {Provider: Provide}, + {Provider: admin.Provide}, + } +} diff --git a/backend_v1/app/http/routes.gen.go b/backend_v1/app/http/routes.gen.go new file mode 100644 index 0000000..f6955e1 --- /dev/null +++ b/backend_v1/app/http/routes.gen.go @@ -0,0 +1,93 @@ +// Code generated by atomctl. DO NOT EDIT. + +// Package http provides HTTP route definitions and registration +// for the quyun/v2 application. +package http + +import ( + "quyun/v2/app/middlewares" + "quyun/v2/app/requests" + "quyun/v2/database/models" + + "github.com/gofiber/fiber/v3" + log "github.com/sirupsen/logrus" + _ "go.ipao.vip/atom" + _ "go.ipao.vip/atom/contracts" + . "go.ipao.vip/atom/fen" +) + +// Routes implements the HttpRoute contract and provides route registration +// for all controllers in the http module. +// +// @provider contracts.HttpRoute atom.GroupRoutes +type Routes struct { + log *log.Entry `inject:"false"` + middlewares *middlewares.Middlewares + // Controller instances + posts *posts + users *users +} + +// Prepare initializes the routes provider with logging configuration. +func (r *Routes) Prepare() error { + r.log = log.WithField("module", "routes.http") + r.log.Info("Initializing routes module") + return nil +} + +// Name returns the unique identifier for this routes provider. +func (r *Routes) Name() string { + return "http" +} + +// Register registers all HTTP routes with the provided fiber router. +// Each route is registered with its corresponding controller action and parameter bindings. +func (r *Routes) Register(router fiber.Router) { + // Register routes for controller: posts + r.log.Debugf("Registering route: Get /posts -> posts.List") + router.Get("/posts"[len(r.Path()):], DataFunc3( + r.posts.List, + Query[requests.Pagination]("pagination"), + Query[ListQuery]("query"), + Local[*models.User]("user"), + )) + r.log.Debugf("Registering route: Get /posts/:id/play -> posts.Play") + router.Get("/posts/:id/play"[len(r.Path()):], DataFunc2( + r.posts.Play, + PathParam[int64]("id"), + Local[*models.User]("user"), + )) + r.log.Debugf("Registering route: Get /posts/:id/show -> posts.Show") + router.Get("/posts/:id/show"[len(r.Path()):], DataFunc2( + r.posts.Show, + PathParam[int64]("id"), + Local[*models.User]("user"), + )) + r.log.Debugf("Registering route: Get /posts/mine -> posts.Mine") + router.Get("/posts/mine"[len(r.Path()):], DataFunc3( + r.posts.Mine, + Query[requests.Pagination]("pagination"), + Query[ListQuery]("query"), + Local[*models.User]("user"), + )) + r.log.Debugf("Registering route: Post /posts/:id/buy -> posts.Buy") + router.Post("/posts/:id/buy"[len(r.Path()):], DataFunc2( + r.posts.Buy, + PathParam[int64]("id"), + Local[*models.User]("user"), + )) + // Register routes for controller: users + r.log.Debugf("Registering route: Get /users/profile -> users.Profile") + router.Get("/users/profile"[len(r.Path()):], DataFunc1( + r.users.Profile, + Local[*models.User]("user"), + )) + r.log.Debugf("Registering route: Put /users/username -> users.Update") + router.Put("/users/username"[len(r.Path()):], Func2( + r.users.Update, + Local[*models.User]("user"), + Body[ProfileForm]("form"), + )) + + r.log.Info("Successfully registered all routes") +} diff --git a/backend_v1/app/http/routes.manual.go b/backend_v1/app/http/routes.manual.go new file mode 100644 index 0000000..cb8df6d --- /dev/null +++ b/backend_v1/app/http/routes.manual.go @@ -0,0 +1,9 @@ +package http + +func (r *Routes) Path() string { + return "/http" +} + +func (r *Routes) Middlewares() []any { + return []any{} +} diff --git a/backend_v1/app/http/users.go b/backend_v1/app/http/users.go new file mode 100644 index 0000000..dad47d1 --- /dev/null +++ b/backend_v1/app/http/users.go @@ -0,0 +1,59 @@ +package http + +import ( + "strings" + "time" + + "quyun/v2/app/services" + "quyun/v2/database/models" + + "github.com/gofiber/fiber/v3" +) + +// @provider +type users struct{} + +type UserInfo struct { + ID int64 `json:"id,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + Username string `json:"username,omitempty"` + Avatar string `json:"avatar,omitempty"` + Balance int64 `json:"balance"` +} + +// @Router /users/profile [get] +// @Bind user local +func (ctl *users) Profile(ctx fiber.Ctx, user *models.User) (*UserInfo, error) { + return &UserInfo{ + ID: user.ID, + CreatedAt: user.CreatedAt, + Username: user.Username, + Avatar: user.Avatar, + Balance: user.Balance, + }, nil +} + +type ProfileForm struct { + Username string `json:"username" validate:"required"` +} + +// Update +// +// @Router /users/username [put] +// @Bind user local +// @Bind form body +func (ctl *users) Update(ctx fiber.Ctx, user *models.User, form *ProfileForm) error { + username := strings.TrimSpace(form.Username) + if len([]rune(username)) > 12 { + return fiber.NewError(fiber.StatusBadRequest, "Username exceeds maximum length of 12 characters") + } + + if username == "" { + return fiber.NewError(fiber.StatusBadRequest, "Username cannot be empty") + } + + if err := services.Users.SetUsername(ctx, user.ID, username); err != nil { + return err + } + return nil +} diff --git a/backend_v1/app/http/v1/demo.go b/backend_v1/app/http/v1/demo.go index a176b60..25100d3 100644 --- a/backend_v1/app/http/v1/demo.go +++ b/backend_v1/app/http/v1/demo.go @@ -3,9 +3,7 @@ package v1 import ( "mime/multipart" - "quyun/v2/app/errorx" "quyun/v2/app/requests" - "quyun/v2/app/services" "quyun/v2/providers/jwt" "github.com/gofiber/fiber/v3" @@ -63,14 +61,14 @@ func (d *demo) Foo( file *multipart.FileHeader, req *FooUploadReq, ) error { - _, err := services.Test.Test(ctx) - if err != nil { - // 示例:在控制器层自定义错误消息/附加数据 - appErr := errorx.Wrap(err). - WithMsg("获取测试失败"). - WithData(fiber.Map{"route": "/v1/test"}). - WithParams("handler", "Test.Hello") - return appErr - } + // _, err := services.Test.Test(ctx) + // if err != nil { + // // 示例:在控制器层自定义错误消息/附加数据 + // appErr := errorx.Wrap(err). + // WithMsg("获取测试失败"). + // WithData(fiber.Map{"route": "/v1/test"}). + // WithParams("handler", "Test.Hello") + // return appErr + // } return nil } diff --git a/backend_v1/app/jobs/balance_pay_notify.go b/backend_v1/app/jobs/balance_pay_notify.go index 84e62b4..e9265b6 100644 --- a/backend_v1/app/jobs/balance_pay_notify.go +++ b/backend_v1/app/jobs/balance_pay_notify.go @@ -5,6 +5,10 @@ import ( "fmt" "time" + "quyun/v2/app/services" + "quyun/v2/database/models" + "quyun/v2/pkg/fields" + "github.com/pkg/errors" . "github.com/riverqueue/river" log "github.com/sirupsen/logrus" @@ -41,7 +45,7 @@ func (w *BalancePayNotifyWorker) Work(ctx context.Context, job *Job[BalancePayNo log.Infof("[Start] Working on job with strings: %+v", job.Args) defer log.Infof("[End] Finished %s", job.Args.Kind()) - order, err := model.OrdersModel().GetByOrderNo(context.Background(), job.Args.OrderNo) + order, err := services.Orders.GetByOrderNO(ctx, job.Args.OrderNo) if err != nil { log.Errorf("GetByOrderNo error:%v", err) return err @@ -52,7 +56,7 @@ func (w *BalancePayNotifyWorker) Work(ctx context.Context, job *Job[BalancePayNo return JobCancel(fmt.Errorf("Order already paid, currently status: %d", order.Status)) } - user, err := model.UsersModel().GetByID(context.Background(), order.UserID) + user, err := services.Users.FindByID(ctx, order.UserID) if err != nil { log.Errorf("GetByID error:%v", err) return errors.Wrap(err, "get user error") @@ -63,7 +67,7 @@ func (w *BalancePayNotifyWorker) Work(ctx context.Context, job *Job[BalancePayNo order.PaymentMethod = "balance" order.Status = fields.OrderStatusCompleted - meta := order.Meta.Data + meta := order.Meta.Data() if user.Balance-meta.CostBalance < 0 { log.Errorf("User %d balance is not enough, current balance: %d, cost: %d", user.ID, user.Balance, payPrice) @@ -73,28 +77,29 @@ func (w *BalancePayNotifyWorker) Work(ctx context.Context, job *Job[BalancePayNo } log.Infof("Updated order details: %+v", order) - tx, err := model.Transaction(ctx) + tx := models.Q.Begin() if err != nil { return errors.Wrap(err, "Transaction error") } defer tx.Rollback() // update user balance - err = user.SetBalance(ctx, user.Balance-payPrice) + err = services.Users.DescBalance(ctx, user.ID, payPrice) if err != nil { log.WithError(err).Error("SetBalance error") return JobCancel(errors.Wrap(err, "set user balance failed")) } - if err := user.BuyPosts(context.Background(), order.PostID, order.Price); err != nil { + if err := services.Users.BuyPosts(context.Background(), user.ID, order.PostID, order.Price); err != nil { log.Errorf("BuyPosts error:%v", err) return errors.Wrap(err, "BuyPosts error") } - if err := order.Update(context.Background()); err != nil { + if _, err := order.Update(ctx); err != nil { log.Errorf("Update order error:%v", err) return errors.Wrap(err, "Update order error") } + if err := tx.Commit(); err != nil { log.Errorf("Commit error:%v", err) return errors.Wrap(err, "Commit error") diff --git a/backend_v1/app/jobs/download_from_alioss.go b/backend_v1/app/jobs/download_from_alioss.go index 1ff6cd9..1b06e89 100644 --- a/backend_v1/app/jobs/download_from_alioss.go +++ b/backend_v1/app/jobs/download_from_alioss.go @@ -6,16 +6,14 @@ import ( "path/filepath" "time" - "quyun/v2/app/model" + "quyun/v2/app/services" "quyun/v2/providers/ali" "quyun/v2/providers/app" "quyun/v2/providers/job" . "github.com/riverqueue/river" log "github.com/sirupsen/logrus" - _ "go.ipao.vip/atom" "go.ipao.vip/atom/contracts" - _ "go.ipao.vip/atom/contracts" ) var _ contracts.JobArgs = (*DownloadFromAliOSS)(nil) @@ -55,7 +53,7 @@ func (w *DownloadFromAliOSSWorker) Work(ctx context.Context, job *Job[DownloadFr log.Infof("[Start] Working on job with strings: %+v", job.Args) defer log.Infof("[End] Finished %s", job.Args.Kind()) - media, err := model.MediasModel().GetByHash(ctx, job.Args.MediaHash) + media, err := services.Medias.GetByHash(ctx, job.Args.MediaHash) if err != nil { log.Errorf("Error getting media by ID: %v", err) return JobCancel(err) diff --git a/backend_v1/app/jobs/download_from_alioss_test.go b/backend_v1/app/jobs/download_from_alioss_test.go index 26b08e9..21cf669 100644 --- a/backend_v1/app/jobs/download_from_alioss_test.go +++ b/backend_v1/app/jobs/download_from_alioss_test.go @@ -5,7 +5,6 @@ import ( "testing" "quyun/v2/app/commands/testx" - "quyun/v2/app/model" "quyun/v2/providers/ali" "quyun/v2/providers/app" "quyun/v2/providers/job" @@ -34,7 +33,7 @@ type DownloadFromAliOSSSuite struct { } func Test_DownloadFromAliOSS(t *testing.T) { - providers := testx.Default().With(Provide, model.Provide) + providers := testx.Default().With(Provide) testx.Serve(providers, t, func(p DownloadFromAliOSSSuiteInjectParams) { suite.Run(t, &DownloadFromAliOSSSuite{DownloadFromAliOSSSuiteInjectParams: p}) diff --git a/backend_v1/app/jobs/publish_draft_posts.go b/backend_v1/app/jobs/publish_draft_posts.go index 42c522f..43d60d7 100644 --- a/backend_v1/app/jobs/publish_draft_posts.go +++ b/backend_v1/app/jobs/publish_draft_posts.go @@ -4,6 +4,9 @@ import ( "context" "time" + "quyun/v2/app/services" + "quyun/v2/database/models" + "quyun/v2/pkg/fields" "quyun/v2/pkg/utils" "quyun/v2/providers/ali" "quyun/v2/providers/app" @@ -15,6 +18,7 @@ import ( log "github.com/sirupsen/logrus" _ "go.ipao.vip/atom" "go.ipao.vip/atom/contracts" + "go.ipao.vip/gen/types" ) var _ contracts.JobArgs = (*PublishDraftPosts)(nil) @@ -54,33 +58,33 @@ func (w *PublishDraftPostsWorker) Work(ctx context.Context, job *Job[PublishDraf log.Infof("[Start] Working on job with strings: %+v", job.Args) defer log.Infof("[End] Finished %s", job.Args.Kind()) - media, err := model.MediasModel().GetByHash(ctx, job.Args.MediaHash) + media, err := services.Medias.GetByHash(ctx, job.Args.MediaHash) if err != nil { log.Errorf("Error getting media by ID: %v", err) return JobCancel(err) } - relationMedias, err := model.MediasModel().GetRelations(ctx, media.Hash) + relationMedias, err := services.Medias.GetRelations(ctx, media.Hash) if err != nil { log.Errorf("Error getting relation medias: %v", err) return JobCancel(err) } - assets := lo.FilterMap(relationMedias, func(media *model.Medias, _ int) (fields.MediaAsset, bool) { + assets := lo.FilterMap(relationMedias, func(media *models.Media, _ int) (fields.MediaAsset, bool) { return fields.MediaAsset{ Type: media.MimeType, Media: media.ID, - Metas: &media.Metas.Data, + Metas: lo.ToPtr(media.Metas.Data()), }, media.MimeType != "image/jpeg" }) assets = append(assets, fields.MediaAsset{ Type: media.MimeType, Media: media.ID, - Metas: &media.Metas.Data, + Metas: lo.ToPtr(media.Metas.Data()), }) // publish a draft posts - post := &model.Posts{ + post := &models.Post{ Status: fields.PostStatusDraft, Title: utils.FormatTitle(media.Name), Description: "", @@ -89,9 +93,9 @@ func (w *PublishDraftPostsWorker) Work(ctx context.Context, job *Job[PublishDraf Discount: 100, Views: 0, Likes: 0, - Tags: fields.Json[[]string]{}, - Assets: fields.ToJson(assets), - HeadImages: fields.ToJson(lo.FilterMap(relationMedias, func(media *model.Medias, _ int) (int64, bool) { + Tags: types.NewJSONType([]string{}), + Assets: types.NewJSONType(assets), + HeadImages: types.NewJSONType(lo.FilterMap(relationMedias, func(media *models.Media, _ int) (int64, bool) { return media.ID, media.MimeType == "image/jpeg" })), } diff --git a/backend_v1/app/jobs/video_cut.go b/backend_v1/app/jobs/video_cut.go index 44cc45b..ddcc147 100644 --- a/backend_v1/app/jobs/video_cut.go +++ b/backend_v1/app/jobs/video_cut.go @@ -5,8 +5,8 @@ import ( "path/filepath" "time" - "quyun/v2/app/model" - "quyun/v2/database/fields" + "quyun/v2/app/services" + "quyun/v2/pkg/fields" "quyun/v2/pkg/utils" "quyun/v2/providers/app" "quyun/v2/providers/job" @@ -54,7 +54,7 @@ func (w *VideoCutWorker) Work(ctx context.Context, job *Job[VideoCut]) error { log.Infof("[Start] Working on job with strings: %+v", job.Args) defer log.Infof("[End] Finished %s", job.Args.Kind()) - media, err := model.MediasModel().GetByHash(ctx, job.Args.MediaHash) + media, err := services.Medias.GetByHash(ctx, job.Args.MediaHash) if err != nil { log.Errorf("Error getting media by ID: %v", err) return JobCancel(err) @@ -81,7 +81,7 @@ func (w *VideoCutWorker) Work(ctx context.Context, job *Job[VideoCut]) error { Short: false, Duration: duration, } - if err := model.MediasModel().UpdateMetas(ctx, media.ID, metas); err != nil { + if err := services.Medias.UpdateMetas(ctx, media.ID, metas); err != nil { log.Errorf("Error updating media metas: %v", err) return errors.Wrap(err, "update media metas") } diff --git a/backend_v1/app/jobs/video_extract_head_image.go b/backend_v1/app/jobs/video_extract_head_image.go index 212196b..021561b 100644 --- a/backend_v1/app/jobs/video_extract_head_image.go +++ b/backend_v1/app/jobs/video_extract_head_image.go @@ -6,8 +6,9 @@ import ( "path/filepath" "time" - "quyun/v2/app/model" - "quyun/v2/database/fields" + "quyun/v2/app/services" + "quyun/v2/database/models" + "quyun/v2/pkg/fields" "quyun/v2/pkg/utils" "quyun/v2/providers/ali" "quyun/v2/providers/app" @@ -18,6 +19,7 @@ import ( log "github.com/sirupsen/logrus" _ "go.ipao.vip/atom" "go.ipao.vip/atom/contracts" + "go.ipao.vip/gen/types" ) var _ contracts.JobArgs = (*VideoExtractHeadImage)(nil) @@ -57,7 +59,7 @@ func (w *VideoExtractHeadImageWorker) Work(ctx context.Context, job *Job[VideoEx log.Infof("[Start] Working on job with strings: %+v", job.Args) defer log.Infof("[End] Finished %s", job.Args.Kind()) - media, err := model.MediasModel().GetByHash(ctx, job.Args.MediaHash) + media, err := services.Medias.GetByHash(ctx, job.Args.MediaHash) if err != nil { log.Errorf("Error getting media by ID: %v", err) return JobCancel(err) @@ -88,13 +90,13 @@ func (w *VideoExtractHeadImageWorker) Work(ctx context.Context, job *Job[VideoEx name := "[展示图]" + media.Name + ".jpg" // create a new media record for the image - imageMedia := &model.Medias{ + imageMedia := &models.Media{ Name: name, MimeType: "image/jpeg", Size: fileSize, Path: w.oss.GetSavePath(filename), Hash: fileMd5, - Metas: fields.ToJson(fields.MediaMetas{ + Metas: types.NewJSONType(fields.MediaMetas{ ParentHash: media.Hash, }), } diff --git a/backend_v1/app/jobs/video_store_short.go b/backend_v1/app/jobs/video_store_short.go index 8e02003..c86ac7d 100644 --- a/backend_v1/app/jobs/video_store_short.go +++ b/backend_v1/app/jobs/video_store_short.go @@ -5,8 +5,9 @@ import ( "path/filepath" "time" - "quyun/v2/app/model" - "quyun/v2/database/fields" + "quyun/v2/app/services" + "quyun/v2/database/models" + "quyun/v2/pkg/fields" "quyun/v2/pkg/utils" "quyun/v2/providers/ali" "quyun/v2/providers/app" @@ -17,6 +18,7 @@ import ( log "github.com/sirupsen/logrus" _ "go.ipao.vip/atom" "go.ipao.vip/atom/contracts" + "go.ipao.vip/gen/types" ) var _ contracts.JobArgs = (*VideoStoreShort)(nil) @@ -57,7 +59,7 @@ func (w *VideoStoreShortWorker) Work(ctx context.Context, job *Job[VideoStoreSho log.Infof("[Start] Working on job with strings: %+v", job.Args) defer log.Infof("[End] Finished %s", job.Args.Kind()) - media, err := model.MediasModel().GetByHash(ctx, job.Args.MediaHash) + media, err := services.Medias.GetByHash(ctx, job.Args.MediaHash) if err != nil { log.Errorf("Error getting media by ID: %v", err) return JobCancel(err) @@ -90,13 +92,13 @@ func (w *VideoStoreShortWorker) Work(ctx context.Context, job *Job[VideoStoreSho log.Infof("got file size %s %d", job.Args.FilePath, fileSize) // save to db and relate to master - mediaModel := &model.Medias{ + mediaModel := &models.Media{ Name: "[试听] " + media.Name, MimeType: media.MimeType, Size: fileSize, Path: filePath, Hash: fileMd5, - Metas: fields.ToJson(fields.MediaMetas{ + Metas: types.NewJSONType(fields.MediaMetas{ ParentHash: media.Hash, Short: true, Duration: duration, diff --git a/backend_v1/app/services/medias.go b/backend_v1/app/services/medias.go index c374187..7b45d2b 100644 --- a/backend_v1/app/services/medias.go +++ b/backend_v1/app/services/medias.go @@ -86,3 +86,22 @@ func (m *medias) GetRelations(ctx context.Context, hash string) ([]*models.Media tbl, query := models.MediaQuery.QueryContext(ctx) return query.Where(tbl.Metas.KeyEq("parent_hash", hash)).Find() } + +// FindByID +func (m *medias) FindByID(ctx context.Context, id int64) (*models.Media, error) { + tbl, query := models.MediaQuery.QueryContext(ctx) + item, err := query.Where(tbl.ID.Eq(id)).First() + if err != nil { + return nil, errors.Wrapf(err, "failed to find media by id: %d", id) + } + return item, nil +} + +// Count +func (m *medias) Count(ctx context.Context, conds ...gen.Condition) (int64, error) { + _, query := models.MediaQuery.QueryContext(ctx) + if len(conds) > 0 { + query = query.Where(conds...) + } + return query.Count() +} diff --git a/backend_v1/app/services/orders.go b/backend_v1/app/services/orders.go index 65f30e8..4c9bfe2 100644 --- a/backend_v1/app/services/orders.go +++ b/backend_v1/app/services/orders.go @@ -2,6 +2,7 @@ package services import ( "context" + "time" "quyun/v2/app/requests" "quyun/v2/database/models" @@ -10,6 +11,7 @@ import ( "github.com/pkg/errors" "github.com/samber/lo" "go.ipao.vip/gen" + "go.ipao.vip/gen/types" ) // @provider @@ -19,21 +21,12 @@ type orders struct{} func (m *orders) List( ctx context.Context, pagination *requests.Pagination, - orderNumber *string, - userID *int64, + conds ...gen.Condition, ) (*requests.Pager, error) { pagination.Format() tbl, query := models.OrderQuery.QueryContext(ctx) - conds := make([]gen.Condition, 0, 2) - if orderNumber != nil && *orderNumber != "" { - conds = append(conds, tbl.OrderNo.Like("%"+*orderNumber+"%")) - } - if userID != nil { - conds = append(conds, tbl.UserID.Eq(*userID)) - } - orders, cnt, err := query. Where(conds...). Order(tbl.ID.Desc()). @@ -124,10 +117,79 @@ func (m *orders) Refund(ctx context.Context, id int64) error { return nil }) - } // GetByOrderNO func (m *orders) GetByOrderNO(ctx context.Context, orderNo string) (*models.Order, error) { return models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.OrderNo.Eq(orderNo)).First() } + +func (o *orders) CreateFromUserPostID(ctx context.Context, userId, postId int64) (*models.Order, error) { + post, err := Posts.FindByID(ctx, postId) + if err != nil { + return nil, errors.Wrap(err, "failed to get post") + } + + m := &models.Order{} + m.Status = fields.OrderStatusPending + m.OrderNo = time.Now().Format("20060102150405") + m.SubOrderNo = m.OrderNo + m.UserID = userId + m.PostID = postId + m.Meta = types.NewJSONType(fields.OrderMeta{}) + m.Price = post.Price + m.Discount = post.Discount + + if err := m.Create(ctx); err != nil { + return m, err + } + return m, nil +} + +// FindByID +func (m *orders) FindByID(ctx context.Context, orderID int64) (*models.Order, error) { + return models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(orderID)).First() +} + +func (m *orders) SetMeta(ctx context.Context, orderID int64, metaFunc func(fields.OrderMeta) fields.OrderMeta) error { + order, err := m.FindByID(ctx, orderID) + if err != nil { + return err + } + order.Meta = types.NewJSONType(metaFunc(order.Meta.Data())) + _, err = order.Update(ctx) + + return err +} + +// SetStatus +func (m *orders) SetStatus(ctx context.Context, orderID int64, status fields.OrderStatus) error { + tbl, query := models.OrderQuery.QueryContext(ctx) + + _, err := query.Where(tbl.ID.Eq(orderID)).Update(tbl.Status, status) + return err +} + +// SumAmount +func (m *orders) SumAmount(ctx context.Context) (int64, error) { + tbl, query := models.OrderQuery.QueryContext(ctx) + var calc struct { + Amount int64 `json:"amount"` + } + + err := query.Select(tbl.Price.Sum().As("amount")).Scan(&calc) + if err != nil { + return 0, errors.Wrap(err, "failed to sum amount") + } + + return calc.Amount, nil +} + +// Count +func (m *orders) Count(ctx context.Context, conds ...gen.Condition) (int64, error) { + _, query := models.OrderQuery.QueryContext(ctx) + if len(conds) > 0 { + query = query.Where(conds...) + } + return query.Count() +} diff --git a/backend_v1/app/services/posts.go b/backend_v1/app/services/posts.go index 7eb22e8..e816bcb 100644 --- a/backend_v1/app/services/posts.go +++ b/backend_v1/app/services/posts.go @@ -2,9 +2,10 @@ package services import ( "context" + "time" + "quyun/v2/app/requests" "quyun/v2/database/models" - "time" "github.com/pkg/errors" "github.com/samber/lo" @@ -141,10 +142,28 @@ func (m *posts) GetPostsMapByIDs(ctx context.Context, ids []int64) (map[int64]*m } // GetMediaByIds -func (m *posts) GetMediaByIds(ctx context.Context, ids []int64) ([]*models.Media, error) { +func (m *posts) GetMediasByIds(ctx context.Context, ids []int64) ([]*models.Media, error) { if len(ids) == 0 { return nil, nil } tbl, query := models.MediaQuery.QueryContext(ctx) return query.Where(tbl.ID.In(ids...)).Find() } + +// FindByID +func (m *posts) FindByID(ctx context.Context, id int64, conds ...gen.Condition) (*models.Post, error) { + tbl, query := models.PostQuery.QueryContext(ctx) + if len(conds) > 0 { + query = query.Where(conds...) + } + return query.Where(tbl.ID.Eq(id)).First() +} + +// Count +func (m *posts) Count(ctx context.Context, conds ...gen.Condition) (int64, error) { + _, query := models.PostQuery.QueryContext(ctx) + if len(conds) > 0 { + query = query.Where(conds...) + } + return query.Count() +} diff --git a/backend_v1/app/services/provider.gen.go b/backend_v1/app/services/provider.gen.go index a60cfee..495bc1f 100755 --- a/backend_v1/app/services/provider.gen.go +++ b/backend_v1/app/services/provider.gen.go @@ -6,7 +6,6 @@ import ( "go.ipao.vip/atom/contracts" "go.ipao.vip/atom/opt" "gorm.io/gorm" - "quyun/v2/providers/wepay" ) func Provide(opts ...opt.Option) error { @@ -17,10 +16,15 @@ func Provide(opts ...opt.Option) error { }); err != nil { return err } - if err := container.Container.Provide(func(wepayClient *wepay.Client) (*orders, error) { - obj := &orders{ - wepay: wepayClient, - } + if err := container.Container.Provide(func() (*orders, error) { + obj := &orders{} + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func() (*posts, error) { + obj := &posts{} return obj, nil }); err != nil { @@ -30,11 +34,15 @@ func Provide(opts ...opt.Option) error { db *gorm.DB, medias *medias, orders *orders, + posts *posts, + users *users, ) (contracts.Initial, error) { obj := &services{ db: db, medias: medias, orders: orders, + posts: posts, + users: users, } if err := obj.Prepare(); err != nil { return nil, err @@ -44,5 +52,12 @@ func Provide(opts ...opt.Option) error { }, atom.GroupInitial); err != nil { return err } + if err := container.Container.Provide(func() (*users, error) { + obj := &users{} + + return obj, nil + }); err != nil { + return err + } return nil } diff --git a/backend_v1/app/services/services.gen.go b/backend_v1/app/services/services.gen.go index a3859df..e3ce276 100644 --- a/backend_v1/app/services/services.gen.go +++ b/backend_v1/app/services/services.gen.go @@ -10,6 +10,8 @@ var _db *gorm.DB var ( Medias *medias Orders *orders + Posts *posts + Users *users ) // @provider(model) @@ -18,6 +20,8 @@ type services struct { // define Services medias *medias orders *orders + posts *posts + users *users } func (svc *services) Prepare() error { @@ -26,6 +30,8 @@ func (svc *services) Prepare() error { // set exported Services here Medias = svc.medias Orders = svc.orders + Posts = svc.posts + Users = svc.users return nil } diff --git a/backend_v1/app/services/users.go b/backend_v1/app/services/users.go new file mode 100644 index 0000000..4234706 --- /dev/null +++ b/backend_v1/app/services/users.go @@ -0,0 +1,226 @@ +package services + +import ( + "context" + + "quyun/v2/app/requests" + "quyun/v2/database/models" + + "github.com/pkg/errors" + "github.com/samber/lo" + "go.ipao.vip/gen" +) + +// @provider +type users struct{} + +// List returns a paginated list of users +func (m *users) List( + ctx context.Context, + pagination *requests.Pagination, + conds ...gen.Condition, +) (*requests.Pager, error) { + pagination.Format() + _, query := models.UserQuery.QueryContext(ctx) + + items, cnt, err := query.Where(conds...).FindByPage(int(pagination.Offset()), int(pagination.Limit)) + if err != nil { + return nil, errors.Wrap(err, "query users error") + } + + return &requests.Pager{ + Items: items, + Total: cnt, + Pagination: *pagination, + }, nil +} + +// PostList returns a paginated list of posts for a user +func (m *users) PostList( + ctx context.Context, + userId int64, + pagination *requests.Pagination, + conds ...gen.Condition, +) (*requests.Pager, error) { + pagination.Format() + // stmt := SELECT(tbl.AllColumns). + // FROM(tbl. + // RIGHT_JOIN( + // tblUserPosts, + // tblUserPosts.PostID.EQ(tbl.ID), + // ), + // ). + // WHERE(CondTrue(cond...)). + // ORDER_BY(tblUserPosts.ID.DESC()). + // LIMIT(pagination.Limit). + // OFFSET(pagination.Offset) + // m.log().Infof("sql: %s", stmt.DebugSql()) + + // var posts []Posts + // err := stmt.QueryContext(ctx, db, &posts) + // if err != nil { + // if errors.Is(err, qrm.ErrNoRows) { + // return &requests.Pager{ + // Items: nil, + // Total: 0, + // Pagination: *pagination, + // }, nil + // } + // m.log().Errorf("error querying posts: %v", err) + // return nil, err + // } + + // // total count + // var cnt struct { + // Cnt int64 + // } + + // stmtCnt := tblUserPosts.SELECT(COUNT(tblUserPosts.ID).AS("cnt")).WHERE(tblUserPosts.UserID.EQ(Int64(userId))) + // m.log().Infof("sql: %s", stmtCnt.DebugSql()) + + // if err := stmtCnt.QueryContext(ctx, db, &cnt); err != nil { + // m.log().Errorf("error counting users: %v", err) + // return nil, err + // } + + // return &requests.Pager{ + // Items: posts, + // Total: cnt.Cnt, + // Pagination: *pagination, + // }, nil + return nil, nil +} + +// GetUsersMapByIDs +func (m *users) GetUsersMapByIDs(ctx context.Context, ids []int64) (map[int64]*models.User, error) { + if len(ids) == 0 { + return nil, nil + } + + tbl, query := models.UserQuery.QueryContext(ctx) + items, err := query.Where(tbl.ID.In(ids...)).Find() + if err != nil { + return nil, errors.Wrapf(err, "failed to get users by ids:%v", ids) + } + + return lo.KeyBy(items, func(item *models.User) int64 { + return item.ID + }), nil +} + +// BatchCheckHasBought checks if the user has bought the given post IDs +func (m *users) BatchCheckHasBought(ctx context.Context, userId int64, postIDs []int64) (map[int64]bool, error) { + tbl, query := models.UserPostQuery.QueryContext(ctx) + userPosts, err := query. + Where( + tbl.UserID.Eq(userId), + tbl.PostID.In(postIDs...), + ). + Find() + if err != nil { + return nil, errors.Wrapf(err, "check user has bought failed, user_id: %d, post_ids: %+v", userId, postIDs) + } + + result := make(map[int64]bool) + for _, postID := range postIDs { + result[postID] = false + } + + for _, post := range userPosts { + result[post.PostID] = true + } + return result, nil +} + +// HasBought +func (m *users) HasBought(ctx context.Context, userID, postID int64) (bool, error) { + tbl, query := models.UserPostQuery.QueryContext(ctx) + cnt, err := query. + Where( + tbl.UserID.Eq(userID), + tbl.PostID.Eq(postID), + ). + Count() + if err != nil { + return false, errors.Wrap(err, "failed to check user bought") + } + return cnt > 0, nil +} + +// SetUsername +func (m *users) SetUsername(ctx context.Context, userID int64, username string) error { + tbl, query := models.UserQuery.QueryContext(ctx) + _, err := query. + Where( + tbl.ID.Eq(userID), + ). + Update(tbl.Username, username) + if err != nil { + return err + } + return nil +} + +// BuyPosts +func (m *users) BuyPosts(ctx context.Context, userID, postID, price int64) error { + model := &models.UserPost{UserID: userID, PostID: postID, Price: price} + return model.Create(ctx) +} + +// RevokePosts +func (m *users) RevokeUserPosts(ctx context.Context, userID, postID int64) error { + tbl, query := models.UserPostQuery.QueryContext(ctx) + _, err := query.Where( + tbl.UserID.Eq(userID), + tbl.PostID.Eq(postID), + ).Delete() + return err +} + +// FindByID +func (m *users) FindByID(ctx context.Context, userID int64) (*models.User, error) { + tbl, query := models.UserQuery.QueryContext(ctx) + user, err := query.Where(tbl.ID.Eq(userID)).First() + if err != nil { + return nil, errors.Wrapf(err, "find by id failed, id: %d", userID) + } + return user, nil +} + +// SetBalance +func (m *users) SetBalance(ctx context.Context, userID, balance int64) error { + tbl, query := models.UserQuery.QueryContext(ctx) + _, err := query.Where(tbl.ID.Eq(userID)).Update(tbl.Balance, balance) + + return err +} + +// AddBalance adds the given amount to the user's balance +func (m *users) AddBalance(ctx context.Context, userID, amount int64) error { + tbl, query := models.UserQuery.QueryContext(ctx) + _, err := query.Where(tbl.ID.Eq(userID)).Inc(tbl.Balance, amount) + return err +} + +// Desc desc the given amount to the user's balance +func (m *users) DescBalance(ctx context.Context, userID, amount int64) error { + user, err := m.FindByID(ctx, userID) + if err != nil { + return err + } + if user.Balance < amount { + return errors.New("balance not enough") + } + tbl, query := models.UserQuery.QueryContext(ctx) + _, err = query.Where(tbl.ID.Eq(userID)).Inc(tbl.Balance, -amount) + return err +} + +// Count +func (m *users) Count(ctx context.Context, conds ...gen.Condition) (int64, error) { + _, query := models.UserQuery.QueryContext(ctx) + if len(conds) > 0 { + query = query.Where(conds...) + } + return query.Count() +} diff --git a/backend_v1/pkg/fields/orders.go b/backend_v1/pkg/fields/orders.go index d154966..28fba75 100644 --- a/backend_v1/pkg/fields/orders.go +++ b/backend_v1/pkg/fields/orders.go @@ -2,6 +2,7 @@ package fields import ( "github.com/go-pay/gopay/wechat/v3" + "go.ipao.vip/gen/types" ) // swagger:enum OrderStatus @@ -14,3 +15,8 @@ type OrderMeta struct { RefundNotify *wechat.V3DecryptRefundResult `json:"refund_notify"` CostBalance int64 `json:"cost_balance"` // 余额支付的金额 } + +// JsonType +func (o OrderMeta) JsonType() types.JSONType[OrderMeta] { + return types.NewJSONType(o) +} diff --git a/backend_v1/pkg/fields/posts.go b/backend_v1/pkg/fields/posts.go index 74f4f31..bdc89d7 100644 --- a/backend_v1/pkg/fields/posts.go +++ b/backend_v1/pkg/fields/posts.go @@ -1,5 +1,7 @@ package fields +import "go.ipao.vip/gen/types" + type MediaAsset struct { Type string `json:"type"` Media int64 `json:"media"` @@ -7,6 +9,11 @@ type MediaAsset struct { Mark *string `json:"mark,omitempty"` } +// JsonType +func (t MediaAsset) JsonType() types.JSONType[MediaAsset] { + return types.NewJSONType(t) +} + // swagger:enum PostStatus // ENUM( draft, published ) type PostStatus int16 diff --git a/backend_v1/pkg/oauth/contracts.go b/backend_v1/pkg/oauth/contracts.go index eab1177..85f5b99 100644 --- a/backend_v1/pkg/oauth/contracts.go +++ b/backend_v1/pkg/oauth/contracts.go @@ -9,4 +9,3 @@ type OAuthInfo interface { GetRefreshToken() string GetExpiredAt() time.Time } - diff --git a/backend_v1/pkg/oauth/wechat.go b/backend_v1/pkg/oauth/wechat.go index 5dc97bb..71e3c2a 100644 --- a/backend_v1/pkg/oauth/wechat.go +++ b/backend_v1/pkg/oauth/wechat.go @@ -37,4 +37,3 @@ func (w *WechatOAuthInfo) GetRefreshToken() string { func (w *WechatOAuthInfo) GetUnionID() string { return w.UnionID } - diff --git a/backend_v1/providers/wepay/pay_test.go b/backend_v1/providers/wepay/pay_test.go index 1ca4e23..5950c96 100644 --- a/backend_v1/providers/wepay/pay_test.go +++ b/backend_v1/providers/wepay/pay_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "quyun/app/service/testx" + "quyun/v2/app/commands/testx" "github.com/go-pay/gopay/wechat/v3" "github.com/go-pay/util/js"