diff --git a/backend/app/errorx/error.go b/backend/app/errorx/error.go index f9af7be..9234537 100644 --- a/backend/app/errorx/error.go +++ b/backend/app/errorx/error.go @@ -44,6 +44,11 @@ func New(code, statusCode int, message string) *Response { } } +func (r *Response) WithMsg(msg string) *Response { + r.Message = msg + return r +} + func (r *Response) Sql(sql string) *Response { r.sql = sql return r diff --git a/backend/app/http/orders/controller.go b/backend/app/http/orders/controller.go deleted file mode 100644 index 2055a36..0000000 --- a/backend/app/http/orders/controller.go +++ /dev/null @@ -1,52 +0,0 @@ -package orders - -import ( - "backend/app/requests" - "backend/database/models/qvyun_v2/public/model" - "backend/providers/jwt" - - "github.com/gofiber/fiber/v3" - "github.com/jinzhu/copier" - "github.com/samber/lo" - log "github.com/sirupsen/logrus" -) - -// @provider -type Controller struct { - svc *Service - log *log.Entry `inject:"false"` -} - -func (c *Controller) Prepare() error { - c.log = log.WithField("module", "orders.Controller") - return nil -} - -// Orders show user orders -// @Router /api/v1/orders [get] -// @Bind claim local -// @Bind pagination query -// @Bind filter query -func (c *Controller) List(ctx fiber.Ctx, claim *jwt.Claims, pagination *requests.Pagination, filter *UserOrderFilter) (*requests.Pager, error) { - pagination.Format() - pager := &requests.Pager{ - Pagination: *pagination, - } - - filter.UserID = claim.UserID - orders, total, err := c.svc.GetOrders(ctx.Context(), pagination, filter) - if err != nil { - return nil, err - } - pager.Total = total - - pager.Items = lo.FilterMap(orders, func(item model.Orders, _ int) (UserOrder, bool) { - var o UserOrder - if err := copier.Copy(&o, item); err != nil { - return o, false - } - return o, true - }) - - return pager, nil -} diff --git a/backend/app/http/orders/controller_order.go b/backend/app/http/orders/controller_order.go new file mode 100644 index 0000000..58e83f6 --- /dev/null +++ b/backend/app/http/orders/controller_order.go @@ -0,0 +1,97 @@ +package orders + +import ( + "backend/app/errorx" + "backend/app/http/posts" + "backend/app/http/tenants" + "backend/app/http/users" + "backend/app/requests" + "backend/database/models/qvyun_v2/public/model" + "backend/providers/jwt" + + "github.com/gofiber/fiber/v3" + "github.com/jinzhu/copier" + "github.com/samber/lo" + log "github.com/sirupsen/logrus" +) + +// @provider +type OrderController struct { + svc *Service + userSvc *users.Service + tenantSvc *tenants.Service + postSvc *posts.Service + log *log.Entry `inject:"false"` +} + +func (c *OrderController) Prepare() error { + c.log = log.WithField("module", "orders.OrderController") + return nil +} + +// Orders show user orders +// @Router /api/v1/orders [get] +// @Bind claim local +// @Bind pagination query +// @Bind filter query +func (c *OrderController) List(ctx fiber.Ctx, claim *jwt.Claims, pagination *requests.Pagination, filter *UserOrderFilter) (*requests.Pager, error) { + pagination.Format() + pager := &requests.Pager{ + Pagination: *pagination, + } + + filter.UserID = claim.UserID + orders, total, err := c.svc.GetOrders(ctx.Context(), pagination, filter) + if err != nil { + return nil, err + } + pager.Total = total + + pager.Items = lo.FilterMap(orders, func(item model.Orders, _ int) (UserOrder, bool) { + var o UserOrder + if err := copier.Copy(&o, item); err != nil { + return o, false + } + return o, true + }) + + return pager, nil +} + +// Create order +// @Router /api/v1/orders [post] +// @Bind claim local +// @Bind hash path +// @Bind tenantSlug cookie key(tenant) +func (c *OrderController) Create(ctx fiber.Ctx, claim *jwt.Claims, tenantSlug, hash string) (*UserOrder, error) { + user, err := c.userSvc.GetUserByID(ctx.Context(), claim.UserID) + if err != nil { + return nil, err + } + + tenant, err := c.tenantSvc.GetTenantBySlug(ctx.Context(), tenantSlug) + if err != nil { + return nil, err + } + + post, err := c.postSvc.GetPostByHash(ctx.Context(), tenant.ID, hash) + if err != nil { + return nil, err + } + + if tenant.ID != post.TenantID { + return nil, errorx.BadRequest + } + + order, err := c.svc.Create(ctx.Context(), user, post) + if err != nil { + return nil, err + } + + var userOrder UserOrder + if err := copier.Copy(&userOrder, order); err != nil { + return nil, err + } + + return &userOrder, nil +} diff --git a/backend/app/http/orders/controller_pay.go b/backend/app/http/orders/controller_pay.go new file mode 100644 index 0000000..cb6afd1 --- /dev/null +++ b/backend/app/http/orders/controller_pay.go @@ -0,0 +1,78 @@ +package orders + +import ( + "backend/app/errorx" + "backend/app/http/posts" + "backend/app/http/tenants" + "backend/app/http/users" + "backend/database/fields" + "backend/database/models/qvyun_v2/public/model" + "backend/providers/jwt" + "backend/providers/pay" + + "github.com/go-pay/gopay/wechat/v3" + "github.com/gofiber/fiber/v3" + log "github.com/sirupsen/logrus" +) + +// @provider +type PayController struct { + svc *Service + pay *pay.Client + userSvc *users.Service + tenantSvc *tenants.Service + postSvc *posts.Service + log *log.Entry `inject:"false"` +} + +func (c *PayController) Prepare() error { + c.log = log.WithField("module", "orders.Controller") + return nil +} + +// JSPay +// @Router /api/v1/orders/pay/:orderID/js [get] +// @Bind claim local +// @Bind orderID path +func (ctl *PayController) JSPay(ctx fiber.Ctx, claim *jwt.Claims, orderID string) (*wechat.JSAPIPayParams, error) { + order, err := ctl.svc.GetUserOrderByOrderID(ctx.Context(), orderID, claim.UserID) + if err != nil { + return nil, err + } + + if order.Status != fields.OrderStatusPending { + return nil, errorx.BadRequest.WithMsg("订单状态异常") + } + + oauths, err := ctl.userSvc.GetUserOAuthChannels(ctx.Context(), claim.UserID) + if err != nil { + return nil, err + } + + var oauth *model.UserOauths + for _, v := range oauths { + if v.Channel == fields.AuthChannelWeChat { + oauth = &v + break + } + } + + if oauth == nil { + return nil, errorx.BadRequest.WithMsg("未绑定微信") + } + + params, err := ctl.pay.WeChat_JSApiPayRequest( + ctx.Context(), + oauth.OpenID, + order.OrderSerial, + order.Title, + order.Amount, + 1, + "/v1/orders/pay/wechat/notify", + ) + if err != nil { + return nil, err + } + + return params, nil +} diff --git a/backend/app/http/orders/provider.gen.go b/backend/app/http/orders/provider.gen.go index e228cba..427a9b4 100755 --- a/backend/app/http/orders/provider.gen.go +++ b/backend/app/http/orders/provider.gen.go @@ -3,6 +3,11 @@ package orders import ( "database/sql" + "backend/app/http/posts" + "backend/app/http/tenants" + "backend/app/http/users" + "backend/providers/pay" + "git.ipao.vip/rogeecn/atom" "git.ipao.vip/rogeecn/atom/container" "git.ipao.vip/rogeecn/atom/contracts" @@ -11,10 +16,16 @@ import ( func Provide(opts ...opt.Option) error { if err := container.Container.Provide(func( + postSvc *posts.Service, svc *Service, - ) (*Controller, error) { - obj := &Controller{ - svc: svc, + tenantSvc *tenants.Service, + userSvc *users.Service, + ) (*OrderController, error) { + obj := &OrderController{ + postSvc: postSvc, + svc: svc, + tenantSvc: tenantSvc, + userSvc: userSvc, } if err := obj.Prepare(); err != nil { return nil, err @@ -25,10 +36,34 @@ func Provide(opts ...opt.Option) error { return err } if err := container.Container.Provide(func( - controller *Controller, + pay *pay.Client, + postSvc *posts.Service, + svc *Service, + tenantSvc *tenants.Service, + userSvc *users.Service, + ) (*PayController, error) { + obj := &PayController{ + pay: pay, + postSvc: postSvc, + svc: svc, + tenantSvc: tenantSvc, + userSvc: userSvc, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + orderController *OrderController, + payController *PayController, ) (contracts.HttpRoute, error) { obj := &Routes{ - controller: controller, + orderController: orderController, + payController: payController, } if err := obj.Prepare(); err != nil { return nil, err diff --git a/backend/app/http/orders/routes.gen.go b/backend/app/http/orders/routes.gen.go index 4c49429..f4a26bf 100644 --- a/backend/app/http/orders/routes.gen.go +++ b/backend/app/http/orders/routes.gen.go @@ -15,8 +15,9 @@ import ( // @provider contracts.HttpRoute atom.GroupRoutes type Routes struct { - log *log.Entry `inject:"false"` - controller *Controller + log *log.Entry `inject:"false"` + orderController *OrderController + payController *PayController } func (r *Routes) Prepare() error { @@ -29,12 +30,26 @@ func (r *Routes) Name() string { } func (r *Routes) Register(router fiber.Router) { - // 注册路由组: Controller + // 注册路由组: OrderController router.Get("/api/v1/orders", DataFunc3( - r.controller.List, + r.orderController.List, Local[*jwt.Claims]("claim"), Query[requests.Pagination]("pagination"), Query[UserOrderFilter]("filter"), )) + router.Post("/api/v1/orders", DataFunc3( + r.orderController.Create, + Local[*jwt.Claims]("claim"), + CookieParam("tenant"), + PathParam[string]("hash"), + )) + + // 注册路由组: PayController + router.Get("/api/v1/orders/pay/:orderID/js", DataFunc2( + r.payController.JSPay, + Local[*jwt.Claims]("claim"), + PathParam[string]("orderID"), + )) + } diff --git a/backend/app/http/orders/service.go b/backend/app/http/orders/service.go index 841020a..db65277 100644 --- a/backend/app/http/orders/service.go +++ b/backend/app/http/orders/service.go @@ -3,10 +3,13 @@ package orders import ( "context" "database/sql" + "time" "backend/app/requests" + "backend/database/fields" "backend/database/models/qvyun_v2/public/model" "backend/database/models/qvyun_v2/public/table" + "backend/pkg/utils" "backend/providers/otel" . "github.com/go-jet/jet/v2/postgres" @@ -75,3 +78,99 @@ func (svc *Service) GetOrders(ctx context.Context, pagination *requests.Paginati } return orders, count.Cnt, nil } + +// CreateOrder +func (svc *Service) Create(ctx context.Context, user *model.Users, post *model.Posts) (*model.Orders, error) { + _, span := otel.Start(ctx, "users.service.CreateOrder") + defer span.End() + span.SetAttributes( + attribute.Int64("post.id", post.ID), + ) + + price := post.Price + if post.Discount != 100 { + price = post.Price * int64(post.Discount) / 100 + } + + m := model.Orders{ + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + TenantID: post.TenantID, + UserID: user.ID, + Type: fields.OrderTypeConsume, + Status: fields.OrderStatusPending, + OrderSerial: svc.generateCreateOrderSerial(ctx), + RemoteOrderSerial: "", + RefundSerial: "", + RemoteRefundSerial: "", + Amount: price, + Currency: "CNY", + Title: post.Title, + Description: new(string), + Meta: fields.ToJson(fields.OrderMeta{ + ObjectID: post.ID, + Price: post.Price, + Discount: post.Discount, + Coupons: nil, + }), + } + + tbl := table.Orders + stmt := tbl.INSERT(tbl.MutableColumns).MODEL(m).RETURNING(tbl.AllColumns) + span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql())) + + var mm model.Orders + if err := stmt.QueryContext(ctx, svc.db, &mm); err != nil { + return nil, err + } + return &mm, nil +} + +// generateCreateOrderSerial +func (svc *Service) generateCreateOrderSerial(ctx context.Context) string { + return utils.GenerateOrderSerial("O") +} + +// generateRefundOrderSerial +func (svc *Service) generateRefundOrderSerial(ctx context.Context) string { + return utils.GenerateOrderSerial("R") +} + +// GetByOrderID +func (svc *Service) GetByOrderID(ctx context.Context, orderID string) (*model.Orders, error) { + _, span := otel.Start(ctx, "users.service.GetByOrderID") + defer span.End() + span.SetAttributes( + attribute.String("order.id", orderID), + ) + + tbl := table.Orders + stmt := tbl.SELECT(tbl.AllColumns).WHERE(tbl.OrderSerial.EQ(String(orderID))) + span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql())) + + var order model.Orders + if err := stmt.QueryContext(ctx, svc.db, &order); err != nil { + return nil, err + } + return &order, nil +} + +// GetUserOrderByOrderID +func (svc *Service) GetUserOrderByOrderID(ctx context.Context, orderID string, userID int64) (*model.Orders, error) { + _, span := otel.Start(ctx, "users.service.GetUserOrderByOrderID") + defer span.End() + span.SetAttributes( + attribute.String("order.id", orderID), + attribute.Int64("user.id", userID), + ) + + tbl := table.Orders + stmt := tbl.SELECT(tbl.AllColumns).WHERE(tbl.OrderSerial.EQ(String(orderID)).AND(tbl.UserID.EQ(Int64(userID)))) + span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql())) + + var order model.Orders + if err := stmt.QueryContext(ctx, svc.db, &order); err != nil { + return nil, err + } + return &order, nil +} diff --git a/backend/app/http/posts/controller.go b/backend/app/http/posts/controller.go index 191235e..99f9ab7 100644 --- a/backend/app/http/posts/controller.go +++ b/backend/app/http/posts/controller.go @@ -1,6 +1,7 @@ package posts import ( + "backend/app/http/tenants" "backend/app/requests" "backend/database/models/qvyun_v2/public/model" "backend/providers/jwt" @@ -13,8 +14,9 @@ import ( // @provider type Controller struct { - svc *Service - log *log.Entry `inject:"false"` + tenantSvc *tenants.Service + svc *Service + log *log.Entry `inject:"false"` } func (c *Controller) Prepare() error { @@ -24,16 +26,22 @@ func (c *Controller) Prepare() error { // List show posts list // @Router /api/v1/posts [get] +// @Bind tenantSlug cookie key(tenant) // @Bind claim local // @Bind pagination query // @Bind filter query -func (c *Controller) List(ctx fiber.Ctx, claim *jwt.Claims, pagination *requests.Pagination, filter *UserPostFilter) (*requests.Pager, error) { +func (c *Controller) List(ctx fiber.Ctx, tenantSlug string, claim *jwt.Claims, pagination *requests.Pagination, filter *UserPostFilter) (*requests.Pager, error) { + tenant, err := c.tenantSvc.GetTenantBySlug(ctx.Context(), tenantSlug) + if err != nil { + return nil, err + } + pagination.Format() pager := &requests.Pager{ Pagination: *pagination, } - filter.TenantID = *claim.TenantID + filter.TenantID = tenant.ID filter.UserID = claim.UserID orders, total, err := c.svc.GetPosts(ctx.Context(), pagination, filter) if err != nil { @@ -51,3 +59,64 @@ func (c *Controller) List(ctx fiber.Ctx, claim *jwt.Claims, pagination *requests return pager, nil } + +// ListBought show user bought posts list +// @Router /api/v1/bought-posts [get] +// @Bind tenantSlug cookie key(tenant) +// @Bind claim local +// @Bind pagination query +// @Bind filter query +func (c *Controller) ListBought(ctx fiber.Ctx, tenantSlug string, claim *jwt.Claims, pagination *requests.Pagination, filter *UserPostFilter) (*requests.Pager, error) { + tenant, err := c.tenantSvc.GetTenantBySlug(ctx.Context(), tenantSlug) + if err != nil { + return nil, err + } + + pagination.Format() + pager := &requests.Pager{ + Pagination: *pagination, + } + + filter.TenantID = tenant.ID + filter.UserID = claim.UserID + orders, total, err := c.svc.GetBoughtPosts(ctx.Context(), pagination, filter) + if err != nil { + return nil, err + } + pager.Total = total + + pager.Items = lo.FilterMap(orders, func(item model.Posts, _ int) (UserPost, bool) { + var o UserPost + if err := copier.Copy(&o, item); err != nil { + return o, false + } + return o, true + }) + + return pager, nil +} + +// Show show posts detail +// @Router /api/v1/show/:hash [get] +// @Bind claim local +// @Bind tenantSlug cookie key(tenant) +// @Bind hash path +func (c *Controller) Show(ctx fiber.Ctx, claim *jwt.Claims, tenantSlug, hash string) (*UserPost, error) { + userPost := &UserPost{} + + tenant, err := c.tenantSvc.GetTenantBySlug(ctx.Context(), tenantSlug) + if err != nil { + return nil, err + } + + post, err := c.svc.GetPostByHash(ctx.Context(), tenant.ID, hash) + if err != nil { + return nil, err + } + + if err := copier.Copy(userPost, post); err != nil { + return nil, err + } + + return userPost, nil +} diff --git a/backend/app/http/posts/provider.gen.go b/backend/app/http/posts/provider.gen.go index c55c455..34161aa 100755 --- a/backend/app/http/posts/provider.gen.go +++ b/backend/app/http/posts/provider.gen.go @@ -3,6 +3,8 @@ package posts import ( "database/sql" + "backend/app/http/tenants" + "git.ipao.vip/rogeecn/atom" "git.ipao.vip/rogeecn/atom/container" "git.ipao.vip/rogeecn/atom/contracts" @@ -12,9 +14,11 @@ import ( func Provide(opts ...opt.Option) error { if err := container.Container.Provide(func( svc *Service, + tenantSvc *tenants.Service, ) (*Controller, error) { obj := &Controller{ - svc: svc, + svc: svc, + tenantSvc: tenantSvc, } if err := obj.Prepare(); err != nil { return nil, err diff --git a/backend/app/http/posts/routes.gen.go b/backend/app/http/posts/routes.gen.go index d24c559..94512b6 100644 --- a/backend/app/http/posts/routes.gen.go +++ b/backend/app/http/posts/routes.gen.go @@ -30,11 +30,27 @@ func (r *Routes) Name() string { func (r *Routes) Register(router fiber.Router) { // 注册路由组: Controller - router.Get("/api/v1/posts", DataFunc3( + router.Get("/api/v1/posts", DataFunc4( r.controller.List, + CookieParam("tenant"), Local[*jwt.Claims]("claim"), Query[requests.Pagination]("pagination"), Query[UserPostFilter]("filter"), )) + router.Get("/api/v1/bought-posts", DataFunc4( + r.controller.ListBought, + CookieParam("tenant"), + Local[*jwt.Claims]("claim"), + Query[requests.Pagination]("pagination"), + Query[UserPostFilter]("filter"), + )) + + router.Get("/api/v1/show/:hash", DataFunc3( + r.controller.Show, + Local[*jwt.Claims]("claim"), + CookieParam("tenant"), + PathParam[string]("hash"), + )) + } diff --git a/backend/app/http/posts/service.go b/backend/app/http/posts/service.go index b970461..44eea9f 100644 --- a/backend/app/http/posts/service.go +++ b/backend/app/http/posts/service.go @@ -11,6 +11,7 @@ import ( "backend/providers/otel" . "github.com/go-jet/jet/v2/postgres" + "github.com/samber/lo" log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/attribute" semconv "go.opentelemetry.io/otel/semconv/v1.4.0" @@ -28,6 +29,78 @@ func (svc *Service) Prepare() error { return nil } +// GetBoughtPosts +func (svc *Service) GetBoughtPosts(ctx context.Context, pagination *requests.Pagination, filter *UserPostFilter) ([]model.Posts, int64, error) { + _, span := otel.Start(ctx, "users.service.GetBoughtPosts") + defer span.End() + span.SetAttributes( + attribute.Int64("user.id", filter.UserID), + attribute.Int64("page.page", pagination.Page), + attribute.Int64("page.limit", pagination.Limit), + ) + tbl := table.Posts + + boughtIds, err := svc.GetUserBoughtIDs(ctx, filter.TenantID, filter.UserID) + if err != nil { + return nil, 0, err + } + + if len(boughtIds) == 0 { + return nil, 0, nil + } + + idExprs := lo.Map(boughtIds, func(id int64, _ int) Expression { return Int64(id) }) + + cond := tbl.ID.IN( + idExprs..., + ).AND( + tbl.TenantID.EQ(Int64(filter.TenantID)), + ).AND( + tbl.UserID.EQ(Int64(filter.UserID)), + ) + + if filter.CreatedAt != nil { + cond = cond.AND(tbl.CreatedAt.LT_EQ(TimestampT(*filter.CreatedAt))) + } + + if filter.Keyword != nil { + cond = cond.AND( + tbl.Title. + LIKE(String(database.WrapLike(*filter.Keyword))). + OR( + tbl.Description.LIKE(String(database.WrapLike(*filter.Keyword))), + ). + OR( + tbl.Content.LIKE(String(database.WrapLike(*filter.Keyword))), + ), + ) + } + + cntStmt := tbl.SELECT(COUNT(tbl.ID).AS("cnt")).WHERE(cond) + span.SetAttributes(semconv.DBStatementKey.String(cntStmt.DebugSql())) + + var count struct { + Cnt int64 + } + if err := cntStmt.QueryContext(ctx, svc.db, &count); err != nil { + return nil, 0, err + } + + stmt := tbl. + SELECT(tbl.AllColumns). + ORDER_BY(tbl.ID.DESC()). + LIMIT(pagination.Limit). + OFFSET(pagination.Offset()) + span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql())) + + var posts []model.Posts + if err := stmt.QueryContext(ctx, svc.db, &posts); err != nil { + return nil, 0, err + } + + return posts, count.Cnt, nil +} + // GetPosts func (svc *Service) GetPosts(ctx context.Context, pagination *requests.Pagination, filter *UserPostFilter) ([]model.Posts, int64, error) { _, span := otel.Start(ctx, "users.service.GetPosts") @@ -89,3 +162,61 @@ func (svc *Service) GetPosts(ctx context.Context, pagination *requests.Paginatio return posts, count.Cnt, nil } + +// GetPostByHash +func (svc *Service) GetPostByHash(ctx context.Context, tenantID int64, hash string) (*model.Posts, error) { + _, span := otel.Start(ctx, "users.service.GetPostByHash") + defer span.End() + span.SetAttributes( + attribute.String("hash", hash), + ) + tbl := table.Posts + + stmt := tbl. + SELECT(tbl.AllColumns). + WHERE( + tbl.Hash.EQ(String(hash)).AND( + tbl.TenantID.EQ(Int64(tenantID)), + ), + ) + span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql())) + + var post model.Posts + if err := stmt.QueryContext(ctx, svc.db, &post); err != nil { + return nil, err + } + + return &post, nil +} + +// GetUserBoughtPosts +func (svc *Service) GetUserBoughtIDs(ctx context.Context, tenantID, userID int64) ([]int64, error) { + _, span := otel.Start(ctx, "users.service.GetUserBoughtIDs") + defer span.End() + span.SetAttributes( + attribute.Int64("tenant.id", tenantID), + attribute.Int64("user.id", userID), + ) + tbl := table.UserBoughtPosts + + stmt := tbl. + SELECT(tbl.PostID.AS("post_id")). + WHERE( + tbl.TenantID.EQ(Int64(tenantID)).AND( + tbl.UserID.EQ(Int64(userID)), + ), + ) + span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql())) + type tmp struct { + PostID int64 + } + + var results []tmp + if err := stmt.QueryContext(ctx, svc.db, &results); err != nil { + return nil, err + } + + return lo.Map(results, func(item tmp, _ int) int64 { + return item.PostID + }), nil +} diff --git a/backend/app/http/tenants/controller.go b/backend/app/http/tenants/controller.go index d98f610..1d44428 100644 --- a/backend/app/http/tenants/controller.go +++ b/backend/app/http/tenants/controller.go @@ -1,9 +1,6 @@ package tenants import ( - "time" - - "backend/app/consts" "backend/providers/jwt" "backend/providers/otel" @@ -40,20 +37,11 @@ func (c *Controller) Index(ctx fiber.Ctx, tenant string, claim *jwt.Claims) erro return err } - if claim.TenantID == nil { - claim.TenantID = &tenantModel.ID - token, err := c.jwt.CreateToken(claim) - if err != nil { - return err - } - - ctx.Cookie(&fiber.Cookie{ - Name: consts.TokenTypeUser.String(), - Value: token, - Expires: time.Now().Add(6 * time.Hour), - HTTPOnly: true, - }) - } + // set tenant cookie + ctx.Cookie(&fiber.Cookie{ + Name: "tenant", + Value: tenantModel.Slug, + }) // TODO: render page return nil diff --git a/backend/config.toml b/backend/config.toml index 8bc9401..b8eb168 100644 --- a/backend/config.toml +++ b/backend/config.toml @@ -28,3 +28,13 @@ Salt = "LiXi.Y@140202" Type = "local" Path = "/mnt/yangpingliang/processed" Asset = "/projects/qvyun/frontend/dist" + + +[Pay] +[Pay.WeChat] +AppId = "wx45745a8c51091ae0" +MechID = "" +SubMechID = "" +SerialNo = "" +ApiV3Key = "" +PrivateKey = "" diff --git a/backend/database/fields/orders.gen.go b/backend/database/fields/orders.gen.go new file mode 100644 index 0000000..109cd8d --- /dev/null +++ b/backend/database/fields/orders.gen.go @@ -0,0 +1,479 @@ +// Code generated by go-enum DO NOT EDIT. +// Version: - +// Revision: - +// Build Date: - +// Built By: - + +package fields + +import ( + "database/sql/driver" + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + // OrderStatusPending is a OrderStatus of type Pending. + OrderStatusPending OrderStatus = iota + // OrderStatusPaid is a OrderStatus of type Paid. + OrderStatusPaid + // OrderStatusRefunding is a OrderStatus of type Refunding. + OrderStatusRefunding + // OrderStatusRefunded is a OrderStatus of type Refunded. + OrderStatusRefunded + // OrderStatusCancelled is a OrderStatus of type Cancelled. + OrderStatusCancelled +) + +var ErrInvalidOrderStatus = fmt.Errorf("not a valid OrderStatus, try [%s]", strings.Join(_OrderStatusNames, ", ")) + +const _OrderStatusName = "PendingPaidRefundingRefundedCancelled" + +var _OrderStatusNames = []string{ + _OrderStatusName[0:7], + _OrderStatusName[7:11], + _OrderStatusName[11:20], + _OrderStatusName[20:28], + _OrderStatusName[28:37], +} + +// OrderStatusNames returns a list of possible string values of OrderStatus. +func OrderStatusNames() []string { + tmp := make([]string, len(_OrderStatusNames)) + copy(tmp, _OrderStatusNames) + return tmp +} + +// OrderStatusValues returns a list of the values for OrderStatus +func OrderStatusValues() []OrderStatus { + return []OrderStatus{ + OrderStatusPending, + OrderStatusPaid, + OrderStatusRefunding, + OrderStatusRefunded, + OrderStatusCancelled, + } +} + +var _OrderStatusMap = map[OrderStatus]string{ + OrderStatusPending: _OrderStatusName[0:7], + OrderStatusPaid: _OrderStatusName[7:11], + OrderStatusRefunding: _OrderStatusName[11:20], + OrderStatusRefunded: _OrderStatusName[20:28], + OrderStatusCancelled: _OrderStatusName[28:37], +} + +// String implements the Stringer interface. +func (x OrderStatus) String() string { + if str, ok := _OrderStatusMap[x]; ok { + return str + } + return fmt.Sprintf("OrderStatus(%d)", x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x OrderStatus) IsValid() bool { + _, ok := _OrderStatusMap[x] + return ok +} + +var _OrderStatusValue = map[string]OrderStatus{ + _OrderStatusName[0:7]: OrderStatusPending, + _OrderStatusName[7:11]: OrderStatusPaid, + _OrderStatusName[11:20]: OrderStatusRefunding, + _OrderStatusName[20:28]: OrderStatusRefunded, + _OrderStatusName[28:37]: OrderStatusCancelled, +} + +// ParseOrderStatus attempts to convert a string to a OrderStatus. +func ParseOrderStatus(name string) (OrderStatus, error) { + if x, ok := _OrderStatusValue[name]; ok { + return x, nil + } + return OrderStatus(0), fmt.Errorf("%s is %w", name, ErrInvalidOrderStatus) +} + +var errOrderStatusNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *OrderStatus) Scan(value interface{}) (err error) { + if value == nil { + *x = OrderStatus(0) + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case int64: + *x = OrderStatus(v) + case string: + *x, err = ParseOrderStatus(v) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(v); verr == nil { + *x, err = OrderStatus(val), nil + } + } + case []byte: + *x, err = ParseOrderStatus(string(v)) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(string(v)); verr == nil { + *x, err = OrderStatus(val), nil + } + } + case OrderStatus: + *x = v + case int: + *x = OrderStatus(v) + case *OrderStatus: + if v == nil { + return errOrderStatusNilPtr + } + *x = *v + case uint: + *x = OrderStatus(v) + case uint64: + *x = OrderStatus(v) + case *int: + if v == nil { + return errOrderStatusNilPtr + } + *x = OrderStatus(*v) + case *int64: + if v == nil { + return errOrderStatusNilPtr + } + *x = OrderStatus(*v) + case float64: // json marshals everything as a float64 if it's a number + *x = OrderStatus(v) + case *float64: // json marshals everything as a float64 if it's a number + if v == nil { + return errOrderStatusNilPtr + } + *x = OrderStatus(*v) + case *uint: + if v == nil { + return errOrderStatusNilPtr + } + *x = OrderStatus(*v) + case *uint64: + if v == nil { + return errOrderStatusNilPtr + } + *x = OrderStatus(*v) + case *string: + if v == nil { + return errOrderStatusNilPtr + } + *x, err = ParseOrderStatus(*v) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(*v); verr == nil { + *x, err = OrderStatus(val), nil + } + } + } + + return +} + +// Value implements the driver Valuer interface. +func (x OrderStatus) Value() (driver.Value, error) { + return int64(x), nil +} + +// Set implements the Golang flag.Value interface func. +func (x *OrderStatus) Set(val string) error { + v, err := ParseOrderStatus(val) + *x = v + return err +} + +// Get implements the Golang flag.Getter interface func. +func (x *OrderStatus) Get() interface{} { + return *x +} + +// Type implements the github.com/spf13/pFlag Value interface. +func (x *OrderStatus) Type() string { + return "OrderStatus" +} + +type NullOrderStatus struct { + OrderStatus OrderStatus + Valid bool +} + +func NewNullOrderStatus(val interface{}) (x NullOrderStatus) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Scan implements the Scanner interface. +func (x *NullOrderStatus) Scan(value interface{}) (err error) { + if value == nil { + x.OrderStatus, x.Valid = OrderStatus(0), false + return + } + + err = x.OrderStatus.Scan(value) + x.Valid = (err == nil) + return +} + +// Value implements the driver Valuer interface. +func (x NullOrderStatus) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + // driver.Value accepts int64 for int values. + return int64(x.OrderStatus), nil +} + +type NullOrderStatusStr struct { + NullOrderStatus +} + +func NewNullOrderStatusStr(val interface{}) (x NullOrderStatusStr) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Value implements the driver Valuer interface. +func (x NullOrderStatusStr) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + return x.OrderStatus.String(), nil +} + +const ( + // OrderTypeCharge is a OrderType of type Charge. + OrderTypeCharge OrderType = iota + // OrderTypeConsume is a OrderType of type Consume. + OrderTypeConsume + // OrderTypeRefund is a OrderType of type Refund. + OrderTypeRefund +) + +var ErrInvalidOrderType = fmt.Errorf("not a valid OrderType, try [%s]", strings.Join(_OrderTypeNames, ", ")) + +const _OrderTypeName = "ChargeConsumeRefund" + +var _OrderTypeNames = []string{ + _OrderTypeName[0:6], + _OrderTypeName[6:13], + _OrderTypeName[13:19], +} + +// OrderTypeNames returns a list of possible string values of OrderType. +func OrderTypeNames() []string { + tmp := make([]string, len(_OrderTypeNames)) + copy(tmp, _OrderTypeNames) + return tmp +} + +// OrderTypeValues returns a list of the values for OrderType +func OrderTypeValues() []OrderType { + return []OrderType{ + OrderTypeCharge, + OrderTypeConsume, + OrderTypeRefund, + } +} + +var _OrderTypeMap = map[OrderType]string{ + OrderTypeCharge: _OrderTypeName[0:6], + OrderTypeConsume: _OrderTypeName[6:13], + OrderTypeRefund: _OrderTypeName[13:19], +} + +// String implements the Stringer interface. +func (x OrderType) String() string { + if str, ok := _OrderTypeMap[x]; ok { + return str + } + return fmt.Sprintf("OrderType(%d)", x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x OrderType) IsValid() bool { + _, ok := _OrderTypeMap[x] + return ok +} + +var _OrderTypeValue = map[string]OrderType{ + _OrderTypeName[0:6]: OrderTypeCharge, + _OrderTypeName[6:13]: OrderTypeConsume, + _OrderTypeName[13:19]: OrderTypeRefund, +} + +// ParseOrderType attempts to convert a string to a OrderType. +func ParseOrderType(name string) (OrderType, error) { + if x, ok := _OrderTypeValue[name]; ok { + return x, nil + } + return OrderType(0), fmt.Errorf("%s is %w", name, ErrInvalidOrderType) +} + +var errOrderTypeNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *OrderType) Scan(value interface{}) (err error) { + if value == nil { + *x = OrderType(0) + return + } + + // A wider range of scannable types. + // driver.Value values at the top of the list for expediency + switch v := value.(type) { + case int64: + *x = OrderType(v) + case string: + *x, err = ParseOrderType(v) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(v); verr == nil { + *x, err = OrderType(val), nil + } + } + case []byte: + *x, err = ParseOrderType(string(v)) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(string(v)); verr == nil { + *x, err = OrderType(val), nil + } + } + case OrderType: + *x = v + case int: + *x = OrderType(v) + case *OrderType: + if v == nil { + return errOrderTypeNilPtr + } + *x = *v + case uint: + *x = OrderType(v) + case uint64: + *x = OrderType(v) + case *int: + if v == nil { + return errOrderTypeNilPtr + } + *x = OrderType(*v) + case *int64: + if v == nil { + return errOrderTypeNilPtr + } + *x = OrderType(*v) + case float64: // json marshals everything as a float64 if it's a number + *x = OrderType(v) + case *float64: // json marshals everything as a float64 if it's a number + if v == nil { + return errOrderTypeNilPtr + } + *x = OrderType(*v) + case *uint: + if v == nil { + return errOrderTypeNilPtr + } + *x = OrderType(*v) + case *uint64: + if v == nil { + return errOrderTypeNilPtr + } + *x = OrderType(*v) + case *string: + if v == nil { + return errOrderTypeNilPtr + } + *x, err = ParseOrderType(*v) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(*v); verr == nil { + *x, err = OrderType(val), nil + } + } + } + + return +} + +// Value implements the driver Valuer interface. +func (x OrderType) Value() (driver.Value, error) { + return int64(x), nil +} + +// Set implements the Golang flag.Value interface func. +func (x *OrderType) Set(val string) error { + v, err := ParseOrderType(val) + *x = v + return err +} + +// Get implements the Golang flag.Getter interface func. +func (x *OrderType) Get() interface{} { + return *x +} + +// Type implements the github.com/spf13/pFlag Value interface. +func (x *OrderType) Type() string { + return "OrderType" +} + +type NullOrderType struct { + OrderType OrderType + Valid bool +} + +func NewNullOrderType(val interface{}) (x NullOrderType) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Scan implements the Scanner interface. +func (x *NullOrderType) Scan(value interface{}) (err error) { + if value == nil { + x.OrderType, x.Valid = OrderType(0), false + return + } + + err = x.OrderType.Scan(value) + x.Valid = (err == nil) + return +} + +// Value implements the driver Valuer interface. +func (x NullOrderType) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + // driver.Value accepts int64 for int values. + return int64(x.OrderType), nil +} + +type NullOrderTypeStr struct { + NullOrderType +} + +func NewNullOrderTypeStr(val interface{}) (x NullOrderTypeStr) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Value implements the driver Valuer interface. +func (x NullOrderTypeStr) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + return x.OrderType.String(), nil +} diff --git a/backend/database/fields/orders.go b/backend/database/fields/orders.go new file mode 100644 index 0000000..98b263f --- /dev/null +++ b/backend/database/fields/orders.go @@ -0,0 +1,19 @@ +package fields + +// swagger:enum OrderType +// ENUM( Charge, Consume, Refund) +type OrderType int16 + +// swagger:enum OrderStatus +// ENUM( Pending, Paid, Refunding, Refunded, Cancelled) +type OrderStatus int16 + +type OrderMeta struct { + ObjectID int64 `json:"object_id"` + Price int64 `json:"price"` + Discount int16 `json:"discount"` + Coupons []struct { + ID int64 `json:"id"` + Description string `json:"description"` + } `json:"coupons"` +} diff --git a/backend/database/migrations/20250109095933_create_post.sql b/backend/database/migrations/20250109095933_create_post.sql index e837266..f396abc 100644 --- a/backend/database/migrations/20250109095933_create_post.sql +++ b/backend/database/migrations/20250109095933_create_post.sql @@ -11,7 +11,7 @@ CREATE TABLE tenant_id INT8 NOT NULL, user_id INT8 NOT NULL, - hash_id VARCHAR(128) NOT NULL, + hash VARCHAR(128) NOT NULL UNIQUE, title VARCHAR(128) NOT NULL, description VARCHAR(256) NOT NULL, poster VARCHAR(128) NOT NULL, @@ -25,6 +25,31 @@ CREATE TABLE meta jsonb default '{}'::jsonb, assets jsonb default '{}'::jsonb ); +-- create indexes +CREATE INDEX posts_tenant_id_index ON posts (tenant_id); +CREATE INDEX posts_user_id_index ON posts (user_id); +CREATE INDEX posts_title_index ON posts (title); + + +-- create user bought posts +CREATE TABLE + user_bought_posts ( + id SERIAL8 PRIMARY KEY, + created_at timestamp NOT NULL default now(), + updated_at timestamp NOT NULL default now(), + + tenant_id INT8 NOT NULL, + user_id INT8 NOT NULL, + post_id INT8 NOT NULL, + price INT8 NOT NULL default 0, + discount INT2 NOT NULL default 100, + meta jsonb default '{}'::jsonb + ); +-- create indexes +CREATE INDEX user_bought_posts_tenant_id_index ON user_bought_posts (tenant_id); +CREATE INDEX user_bought_posts_user_id_index ON user_bought_posts (user_id); +CREATE INDEX user_bought_posts_post_id_index ON user_bought_posts (post_id); + -- +goose StatementEnd -- +goose Down diff --git a/backend/database/migrations/20250110093636_create_medias.sql b/backend/database/migrations/20250110093636_create_medias.sql index 2db7123..a333981 100644 --- a/backend/database/migrations/20250110093636_create_medias.sql +++ b/backend/database/migrations/20250110093636_create_medias.sql @@ -6,6 +6,7 @@ CREATE TABLE medias ( created_at timestamp NOT NULL default now(), updated_at timestamp NOT NULL default now(), + tenant_id INT8 NOT NULL, user_id INT8 NOT NULL, post_id INT8 NOT NULL, storage_id INT8 NOT NULL, @@ -15,6 +16,11 @@ CREATE TABLE medias ( size INT8 NOT NULL default 0, path VARCHAR(255) NOT NULL default '' ); + +CREATE INDEX medias_tenant_id_index ON medias (tenant_id); +CREATE INDEX medias_user_id_index ON medias (user_id); +CREATE INDEX medias_post_id_index ON medias (post_id); +CREATE INDEX medias_storage_id_index ON medias (storage_id); -- +goose StatementEnd -- +goose Down diff --git a/backend/database/models/qvyun_v2/public/model/medias.go b/backend/database/models/qvyun_v2/public/model/medias.go index 058dd98..a81787a 100644 --- a/backend/database/models/qvyun_v2/public/model/medias.go +++ b/backend/database/models/qvyun_v2/public/model/medias.go @@ -15,6 +15,7 @@ type Medias struct { ID int64 `sql:"primary_key" json:"id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` + TenantID int64 `json:"tenant_id"` UserID int64 `json:"user_id"` PostID int64 `json:"post_id"` StorageID int64 `json:"storage_id"` diff --git a/backend/database/models/qvyun_v2/public/model/orders.go b/backend/database/models/qvyun_v2/public/model/orders.go index c91c163..1c66699 100644 --- a/backend/database/models/qvyun_v2/public/model/orders.go +++ b/backend/database/models/qvyun_v2/public/model/orders.go @@ -8,25 +8,26 @@ package model import ( + "backend/database/fields" "time" ) type Orders struct { - ID int64 `sql:"primary_key" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt *time.Time `json:"deleted_at"` - TenantID int64 `json:"tenant_id"` - UserID int64 `json:"user_id"` - Type int16 `json:"type"` - Status int16 `json:"status"` - OrderSerial string `json:"order_serial"` - RemoteOrderSerial string `json:"remote_order_serial"` - RefundSerial string `json:"refund_serial"` - RemoteRefundSerial string `json:"remote_refund_serial"` - Amount int64 `json:"amount"` - Currency string `json:"currency"` - Title string `json:"title"` - Description *string `json:"description"` - Meta *string `json:"meta"` + ID int64 `sql:"primary_key" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt *time.Time `json:"deleted_at"` + TenantID int64 `json:"tenant_id"` + UserID int64 `json:"user_id"` + Type fields.OrderType `json:"type"` + Status fields.OrderStatus `json:"status"` + OrderSerial string `json:"order_serial"` + RemoteOrderSerial string `json:"remote_order_serial"` + RefundSerial string `json:"refund_serial"` + RemoteRefundSerial string `json:"remote_refund_serial"` + Amount int64 `json:"amount"` + Currency string `json:"currency"` + Title string `json:"title"` + Description *string `json:"description"` + Meta fields.Json[fields.OrderMeta] `json:"meta"` } diff --git a/backend/database/models/qvyun_v2/public/model/posts.go b/backend/database/models/qvyun_v2/public/model/posts.go index caf73d4..80cc76c 100644 --- a/backend/database/models/qvyun_v2/public/model/posts.go +++ b/backend/database/models/qvyun_v2/public/model/posts.go @@ -18,7 +18,7 @@ type Posts struct { DeletedAt *time.Time `json:"deleted_at"` TenantID int64 `json:"tenant_id"` UserID int64 `json:"user_id"` - HashID string `json:"hash_id"` + Hash string `json:"hash"` Title string `json:"title"` Description string `json:"description"` Poster string `json:"poster"` diff --git a/backend/database/models/qvyun_v2/public/model/user_bought_posts.go b/backend/database/models/qvyun_v2/public/model/user_bought_posts.go new file mode 100644 index 0000000..e79e927 --- /dev/null +++ b/backend/database/models/qvyun_v2/public/model/user_bought_posts.go @@ -0,0 +1,24 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +import ( + "time" +) + +type UserBoughtPosts struct { + ID int64 `sql:"primary_key" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + TenantID int64 `json:"tenant_id"` + UserID int64 `json:"user_id"` + PostID int64 `json:"post_id"` + Price int64 `json:"price"` + Discount int16 `json:"discount"` + Meta *string `json:"meta"` +} diff --git a/backend/database/models/qvyun_v2/public/table/medias.go b/backend/database/models/qvyun_v2/public/table/medias.go index e5c69e3..0a3c67c 100644 --- a/backend/database/models/qvyun_v2/public/table/medias.go +++ b/backend/database/models/qvyun_v2/public/table/medias.go @@ -20,6 +20,7 @@ type mediasTable struct { ID postgres.ColumnInteger CreatedAt postgres.ColumnTimestamp UpdatedAt postgres.ColumnTimestamp + TenantID postgres.ColumnInteger UserID postgres.ColumnInteger PostID postgres.ColumnInteger StorageID postgres.ColumnInteger @@ -71,6 +72,7 @@ func newMediasTableImpl(schemaName, tableName, alias string) mediasTable { IDColumn = postgres.IntegerColumn("id") CreatedAtColumn = postgres.TimestampColumn("created_at") UpdatedAtColumn = postgres.TimestampColumn("updated_at") + TenantIDColumn = postgres.IntegerColumn("tenant_id") UserIDColumn = postgres.IntegerColumn("user_id") PostIDColumn = postgres.IntegerColumn("post_id") StorageIDColumn = postgres.IntegerColumn("storage_id") @@ -79,8 +81,8 @@ func newMediasTableImpl(schemaName, tableName, alias string) mediasTable { MimeTypeColumn = postgres.StringColumn("mime_type") SizeColumn = postgres.IntegerColumn("size") PathColumn = postgres.StringColumn("path") - allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, UserIDColumn, PostIDColumn, StorageIDColumn, NameColumn, UUIDColumn, MimeTypeColumn, SizeColumn, PathColumn} - mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, UserIDColumn, PostIDColumn, StorageIDColumn, NameColumn, UUIDColumn, MimeTypeColumn, SizeColumn, PathColumn} + allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, TenantIDColumn, UserIDColumn, PostIDColumn, StorageIDColumn, NameColumn, UUIDColumn, MimeTypeColumn, SizeColumn, PathColumn} + mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, TenantIDColumn, UserIDColumn, PostIDColumn, StorageIDColumn, NameColumn, UUIDColumn, MimeTypeColumn, SizeColumn, PathColumn} ) return mediasTable{ @@ -90,6 +92,7 @@ func newMediasTableImpl(schemaName, tableName, alias string) mediasTable { ID: IDColumn, CreatedAt: CreatedAtColumn, UpdatedAt: UpdatedAtColumn, + TenantID: TenantIDColumn, UserID: UserIDColumn, PostID: PostIDColumn, StorageID: StorageIDColumn, diff --git a/backend/database/models/qvyun_v2/public/table/posts.go b/backend/database/models/qvyun_v2/public/table/posts.go index 22f8b6a..e59f31f 100644 --- a/backend/database/models/qvyun_v2/public/table/posts.go +++ b/backend/database/models/qvyun_v2/public/table/posts.go @@ -23,7 +23,7 @@ type postsTable struct { DeletedAt postgres.ColumnTimestamp TenantID postgres.ColumnInteger UserID postgres.ColumnInteger - HashID postgres.ColumnString + Hash postgres.ColumnString Title postgres.ColumnString Description postgres.ColumnString Poster postgres.ColumnString @@ -82,7 +82,7 @@ func newPostsTableImpl(schemaName, tableName, alias string) postsTable { DeletedAtColumn = postgres.TimestampColumn("deleted_at") TenantIDColumn = postgres.IntegerColumn("tenant_id") UserIDColumn = postgres.IntegerColumn("user_id") - HashIDColumn = postgres.StringColumn("hash_id") + HashColumn = postgres.StringColumn("hash") TitleColumn = postgres.StringColumn("title") DescriptionColumn = postgres.StringColumn("description") PosterColumn = postgres.StringColumn("poster") @@ -95,8 +95,8 @@ func newPostsTableImpl(schemaName, tableName, alias string) postsTable { LikesColumn = postgres.IntegerColumn("likes") MetaColumn = postgres.StringColumn("meta") AssetsColumn = postgres.StringColumn("assets") - allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, TenantIDColumn, UserIDColumn, HashIDColumn, TitleColumn, DescriptionColumn, PosterColumn, ContentColumn, StageColumn, StatusColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, MetaColumn, AssetsColumn} - mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, TenantIDColumn, UserIDColumn, HashIDColumn, TitleColumn, DescriptionColumn, PosterColumn, ContentColumn, StageColumn, StatusColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, MetaColumn, AssetsColumn} + allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, TenantIDColumn, UserIDColumn, HashColumn, TitleColumn, DescriptionColumn, PosterColumn, ContentColumn, StageColumn, StatusColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, MetaColumn, AssetsColumn} + mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, DeletedAtColumn, TenantIDColumn, UserIDColumn, HashColumn, TitleColumn, DescriptionColumn, PosterColumn, ContentColumn, StageColumn, StatusColumn, PriceColumn, DiscountColumn, ViewsColumn, LikesColumn, MetaColumn, AssetsColumn} ) return postsTable{ @@ -109,7 +109,7 @@ func newPostsTableImpl(schemaName, tableName, alias string) postsTable { DeletedAt: DeletedAtColumn, TenantID: TenantIDColumn, UserID: UserIDColumn, - HashID: HashIDColumn, + Hash: HashColumn, Title: TitleColumn, Description: DescriptionColumn, Poster: PosterColumn, diff --git a/backend/database/models/qvyun_v2/public/table/table_use_schema.go b/backend/database/models/qvyun_v2/public/table/table_use_schema.go index 7905e99..5890fa6 100644 --- a/backend/database/models/qvyun_v2/public/table/table_use_schema.go +++ b/backend/database/models/qvyun_v2/public/table/table_use_schema.go @@ -23,6 +23,7 @@ func UseSchema(schema string) { TenantUserBalances = TenantUserBalances.FromSchema(schema) TenantUsers = TenantUsers.FromSchema(schema) Tenants = Tenants.FromSchema(schema) + UserBoughtPosts = UserBoughtPosts.FromSchema(schema) UserOauths = UserOauths.FromSchema(schema) Users = Users.FromSchema(schema) } diff --git a/backend/database/models/qvyun_v2/public/table/user_bought_posts.go b/backend/database/models/qvyun_v2/public/table/user_bought_posts.go new file mode 100644 index 0000000..b6d8cad --- /dev/null +++ b/backend/database/models/qvyun_v2/public/table/user_bought_posts.go @@ -0,0 +1,99 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var UserBoughtPosts = newUserBoughtPostsTable("public", "user_bought_posts", "") + +type userBoughtPostsTable struct { + postgres.Table + + // Columns + ID postgres.ColumnInteger + CreatedAt postgres.ColumnTimestamp + UpdatedAt postgres.ColumnTimestamp + TenantID postgres.ColumnInteger + UserID postgres.ColumnInteger + PostID postgres.ColumnInteger + Price postgres.ColumnInteger + Discount postgres.ColumnInteger + Meta postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type UserBoughtPostsTable struct { + userBoughtPostsTable + + EXCLUDED userBoughtPostsTable +} + +// AS creates new UserBoughtPostsTable with assigned alias +func (a UserBoughtPostsTable) AS(alias string) *UserBoughtPostsTable { + return newUserBoughtPostsTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new UserBoughtPostsTable with assigned schema name +func (a UserBoughtPostsTable) FromSchema(schemaName string) *UserBoughtPostsTable { + return newUserBoughtPostsTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new UserBoughtPostsTable with assigned table prefix +func (a UserBoughtPostsTable) WithPrefix(prefix string) *UserBoughtPostsTable { + return newUserBoughtPostsTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new UserBoughtPostsTable with assigned table suffix +func (a UserBoughtPostsTable) WithSuffix(suffix string) *UserBoughtPostsTable { + return newUserBoughtPostsTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newUserBoughtPostsTable(schemaName, tableName, alias string) *UserBoughtPostsTable { + return &UserBoughtPostsTable{ + userBoughtPostsTable: newUserBoughtPostsTableImpl(schemaName, tableName, alias), + EXCLUDED: newUserBoughtPostsTableImpl("", "excluded", ""), + } +} + +func newUserBoughtPostsTableImpl(schemaName, tableName, alias string) userBoughtPostsTable { + var ( + IDColumn = postgres.IntegerColumn("id") + CreatedAtColumn = postgres.TimestampColumn("created_at") + UpdatedAtColumn = postgres.TimestampColumn("updated_at") + TenantIDColumn = postgres.IntegerColumn("tenant_id") + UserIDColumn = postgres.IntegerColumn("user_id") + PostIDColumn = postgres.IntegerColumn("post_id") + PriceColumn = postgres.IntegerColumn("price") + DiscountColumn = postgres.IntegerColumn("discount") + MetaColumn = postgres.StringColumn("meta") + allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, TenantIDColumn, UserIDColumn, PostIDColumn, PriceColumn, DiscountColumn, MetaColumn} + mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, TenantIDColumn, UserIDColumn, PostIDColumn, PriceColumn, DiscountColumn, MetaColumn} + ) + + return userBoughtPostsTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + ID: IDColumn, + CreatedAt: CreatedAtColumn, + UpdatedAt: UpdatedAtColumn, + TenantID: TenantIDColumn, + UserID: UserIDColumn, + PostID: PostIDColumn, + Price: PriceColumn, + Discount: DiscountColumn, + Meta: MetaColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/database/transform.yaml b/backend/database/transform.yaml index 6afd0f7..38a5fa1 100644 --- a/backend/database/transform.yaml +++ b/backend/database/transform.yaml @@ -14,3 +14,8 @@ types: storages: type: StorageType + + orders: + type: OrderType + status: OrderStatus + meta: Json[OrderMeta] diff --git a/backend/go.mod b/backend/go.mod index 9a2f68d..9a47eed 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -10,6 +10,7 @@ require ( github.com/ThreeDotsLabs/watermill-redisstream v1.4.2 github.com/ThreeDotsLabs/watermill-sql/v3 v3.1.0 github.com/go-jet/jet/v2 v2.12.0 + github.com/go-pay/gopay v1.5.107 github.com/gofiber/fiber/v3 v3.0.0-beta.4 github.com/gofiber/utils/v2 v2.0.0-beta.7 github.com/golang-jwt/jwt/v4 v4.5.1 @@ -81,6 +82,12 @@ require ( github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/spec v0.20.4 // indirect github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-pay/crypto v0.0.1 // indirect + github.com/go-pay/errgroup v0.0.3 // indirect + github.com/go-pay/smap v0.0.2 // indirect + github.com/go-pay/util v0.0.4 // indirect + github.com/go-pay/xlog v0.0.3 // indirect + github.com/go-pay/xtime v0.0.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gofiber/schema v1.2.0 // indirect github.com/golang/protobuf v1.5.4 // indirect diff --git a/backend/go.sum b/backend/go.sum index ca0b15c..6c50ddd 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -83,6 +83,20 @@ github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-pay/crypto v0.0.1 h1:B6InT8CLfSLc6nGRVx9VMJRBBazFMjr293+jl0lLXUY= +github.com/go-pay/crypto v0.0.1/go.mod h1:41oEIvHMKbNcYlWUlRWtsnC6+ASgh7u29z0gJXe5bes= +github.com/go-pay/errgroup v0.0.3 h1:DB4s8e8oWYDyETKQ1y1riMJ7y29zE1uIsMCSjEOFSbU= +github.com/go-pay/errgroup v0.0.3/go.mod h1:0+4b8mvFMS71MIzsaC+gVvB4x37I93lRb2dqrwuU8x8= +github.com/go-pay/gopay v1.5.107 h1:BZauJyTijvvb2AIMJN0SqOWwjPzssmYkTsjn0NME4P4= +github.com/go-pay/gopay v1.5.107/go.mod h1:Kwv8YPKh9StrJAYMDgnoTkCrPW7f9/O+XgWWlCLvOKc= +github.com/go-pay/smap v0.0.2 h1:kKflYor5T5FgZltPFBMTFfjJvqYMHr5VnIFSEyhVTcA= +github.com/go-pay/smap v0.0.2/go.mod h1:HW9oAo0okuyDYsbpbj5fJFxnNj/BZorRGFw26SxrNWw= +github.com/go-pay/util v0.0.4 h1:TuwSU9o3Qd7m9v1PbzFuIA/8uO9FJnA6P7neG/NwPyk= +github.com/go-pay/util v0.0.4/go.mod h1:Tsdhs8Ib9J9b4+NKNO1PHh5hWHhlg98PthsX0ckq6PM= +github.com/go-pay/xlog v0.0.3 h1:avyMhCL/JgBHreoGx/am/kHxfs1udDOAeVqbmzP/Yes= +github.com/go-pay/xlog v0.0.3/go.mod h1:mH47xbobrdsSHWsmFtSF5agWbMHFP+tK0ZbVCk5OAEw= +github.com/go-pay/xtime v0.0.2 h1:7YR4/iuELsEHpJ6LUO0SVK80hQxDO9MLCfuVYIiTCRM= +github.com/go-pay/xtime v0.0.2/go.mod h1:W1yRbJaSt4CSBcdAtLBQ8xajiN/Pl5hquGczUcUE9xE= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= diff --git a/backend/pkg/utils/order.go b/backend/pkg/utils/order.go new file mode 100644 index 0000000..28dfe5a --- /dev/null +++ b/backend/pkg/utils/order.go @@ -0,0 +1,15 @@ +package utils + +import ( + "fmt" + "math/rand" + "strings" + "time" +) + +// GenerateOrderSerial generate order serial +func GenerateOrderSerial(prefix string) string { + timestamp := time.Now().Format("20060102150405") + randomSuffix := rand.Intn(10000) + return fmt.Sprintf("%s%s%04d", strings.ToUpper(prefix), timestamp, randomSuffix) +} diff --git a/backend/providers/pay/config.go b/backend/providers/pay/config.go new file mode 100644 index 0000000..6c05ab0 --- /dev/null +++ b/backend/providers/pay/config.go @@ -0,0 +1,30 @@ +package pay + +import ( + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/utils/opt" +) + +const DefaultPrefix = "Pay" + +func DefaultProvider() container.ProviderContainer { + return container.ProviderContainer{ + Provider: Provide, + Options: []opt.Option{ + opt.Prefix(DefaultPrefix), + }, + } +} + +type Config struct { + WeChat *wechatPay +} + +type wechatPay struct { + AppId string + MechID string + SubMechID string + SerialNo string + ApiV3Key string + PrivateKey string +} diff --git a/backend/providers/pay/provider.go b/backend/providers/pay/provider.go new file mode 100644 index 0000000..ed9a1a2 --- /dev/null +++ b/backend/providers/pay/provider.go @@ -0,0 +1,48 @@ +package pay + +import ( + "backend/providers/app" + + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/utils/opt" + "github.com/go-pay/gopay" + "github.com/go-pay/gopay/wechat/v3" +) + +func Provide(opts ...opt.Option) error { + o := opt.New(opts...) + var config Config + if err := o.UnmarshalConfig(&config); err != nil { + return err + } + return container.Container.Provide(func(app *app.Config) (*Client, error) { + wechatPay, err := wechat.NewClientV3( + config.WeChat.MechID, + config.WeChat.SerialNo, + config.WeChat.ApiV3Key, + config.WeChat.PrivateKey, + ) + if err != nil { + return nil, err + } + + if err := wechatPay.AutoVerifySign(); err != nil { + return nil, err + } + + if app.IsDevMode() { + wechatPay.DebugSwitch = gopay.DebugOn + } + + return &Client{ + conf: &config, + WeChat: wechatPay, + }, nil + }, o.DiOptions()...) +} + +type Client struct { + conf *Config + + WeChat *wechat.ClientV3 +} diff --git a/backend/providers/pay/wechat.go b/backend/providers/pay/wechat.go new file mode 100644 index 0000000..2987768 --- /dev/null +++ b/backend/providers/pay/wechat.go @@ -0,0 +1,49 @@ +package pay + +import ( + "context" + "errors" + "time" + + "github.com/go-pay/gopay" + "github.com/go-pay/gopay/wechat/v3" +) + +// get js pay prepay id +func (client *Client) WeChat_JSApiPayRequest(ctx context.Context, payerOpenID, orderNo, title string, price, amount int64, notifyUrl string) (*wechat.JSAPIPayParams, error) { + expire := time.Now().Add(10 * time.Minute).Format(time.RFC3339) + // 初始化 BodyMap + bm := make(gopay.BodyMap) + + bm. + Set("sp_appid", client.conf.WeChat.AppId). + Set("sp_mchid", client.conf.WeChat.MechID). + Set("sub_mchid", client.conf.WeChat.SubMechID). + Set("description", title). + Set("out_trade_no", orderNo). + Set("time_expire", expire). + Set("notify_url", notifyUrl). + SetBodyMap("amount", func(bm gopay.BodyMap) { + if amount == 0 { + amount = 1 + } + + bm. + Set("total", amount). + Set("currency", "CNY") + }). + SetBodyMap("payer", func(bm gopay.BodyMap) { + bm.Set("sp_openid", payerOpenID) + }) + + resp, err := client.WeChat.V3TransactionJsapi(ctx, bm) + if err != nil { + return nil, err + } + + if resp.Code != 0 { + return nil, errors.New("获取预支付ID失败") + } + + return client.WeChat.PaySignOfJSAPI(client.conf.WeChat.AppId, resp.Response.PrepayId) +}