From ca1b4cdd1254cbe41315ba4e6d7e271d6dbeb090 Mon Sep 17 00:00:00 2001 From: Rogee Date: Fri, 10 Jan 2025 16:54:33 +0800 Subject: [PATCH] feat: add order module --- backend/app/http/orders/controller.go | 52 +++++++++++++++++ backend/app/http/orders/dto.go | 26 +++++++++ backend/app/http/orders/provider.gen.go | 56 ++++++++++++++++++ backend/app/http/orders/routes.gen.go | 40 +++++++++++++ backend/app/http/orders/service.go | 77 +++++++++++++++++++++++++ backend/app/http/orders/service_test.go | 37 ++++++++++++ backend/app/http/tenants/controller.go | 17 +----- backend/app/http/tenants/routes.gen.go | 2 +- backend/app/http/users/controller.go | 45 +++++++++++---- backend/app/http/users/dto.go | 25 ++++++++ backend/app/http/users/provider.gen.go | 16 +++++ backend/app/http/users/routes.gen.go | 37 ++++++++++++ backend/app/http/users/service.go | 26 +++++++++ backend/app/requests/pagination.go | 8 +-- backend/go.mod | 1 + backend/go.sum | 2 + backend/pkg/f/bind.go | 29 +++++++--- 17 files changed, 456 insertions(+), 40 deletions(-) create mode 100644 backend/app/http/orders/controller.go create mode 100644 backend/app/http/orders/dto.go create mode 100755 backend/app/http/orders/provider.gen.go create mode 100644 backend/app/http/orders/routes.gen.go create mode 100644 backend/app/http/orders/service.go create mode 100644 backend/app/http/orders/service_test.go create mode 100644 backend/app/http/users/routes.gen.go diff --git a/backend/app/http/orders/controller.go b/backend/app/http/orders/controller.go new file mode 100644 index 0000000..2055a36 --- /dev/null +++ b/backend/app/http/orders/controller.go @@ -0,0 +1,52 @@ +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/dto.go b/backend/app/http/orders/dto.go new file mode 100644 index 0000000..45c5dc8 --- /dev/null +++ b/backend/app/http/orders/dto.go @@ -0,0 +1,26 @@ +package orders + +import "time" + +type UserOrder struct { + ID int64 `sql:"primary_key" json:"id"` + CreatedAt time.Time `json:"created_at"` + 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"` + Description *string `json:"description"` + Meta *string `json:"meta"` +} + +type UserOrderFilter struct { + UserID int64 + CreatedAt *time.Time `query:"created_at"` + Type *int16 `query:"type"` + Status *int16 `query:"status"` + Description *string `query:"description"` +} diff --git a/backend/app/http/orders/provider.gen.go b/backend/app/http/orders/provider.gen.go new file mode 100755 index 0000000..e228cba --- /dev/null +++ b/backend/app/http/orders/provider.gen.go @@ -0,0 +1,56 @@ +package orders + +import ( + "database/sql" + + "git.ipao.vip/rogeecn/atom" + "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/contracts" + "git.ipao.vip/rogeecn/atom/utils/opt" +) + +func Provide(opts ...opt.Option) error { + if err := container.Container.Provide(func( + svc *Service, + ) (*Controller, error) { + obj := &Controller{ + svc: svc, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }); err != nil { + return err + } + if err := container.Container.Provide(func( + controller *Controller, + ) (contracts.HttpRoute, error) { + obj := &Routes{ + controller: controller, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }, atom.GroupRoutes); err != nil { + return err + } + if err := container.Container.Provide(func( + db *sql.DB, + ) (*Service, error) { + obj := &Service{ + db: db, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }); err != nil { + return err + } + return nil +} diff --git a/backend/app/http/orders/routes.gen.go b/backend/app/http/orders/routes.gen.go new file mode 100644 index 0000000..818637d --- /dev/null +++ b/backend/app/http/orders/routes.gen.go @@ -0,0 +1,40 @@ +// Code generated by the atomctl ; DO NOT EDIT. + +package orders + +import ( + "backend/app/requests" + . "backend/pkg/f" + "backend/providers/jwt" + + _ "git.ipao.vip/rogeecn/atom" + _ "git.ipao.vip/rogeecn/atom/contracts" + "github.com/gofiber/fiber/v3" + log "github.com/sirupsen/logrus" +) + +// @provider contracts.HttpRoute atom.GroupRoutes +type Routes struct { + log *log.Entry `inject:"false"` + controller *Controller +} + +func (r *Routes) Prepare() error { + r.log = log.WithField("module", "routes.orders") + return nil +} + +func (r *Routes) Name() string { + return "orders" +} + +func (r *Routes) Register(router fiber.Router) { + // 注册路由组: Controller + router.Get("/users/orders", DataFunc3( + r.controller.List, + Local[*jwt.Claims]("claim"), + Query[requests.Pagination]("pagination"), + Query[UserOrderFilter]("filter"), + )) + +} diff --git a/backend/app/http/orders/service.go b/backend/app/http/orders/service.go new file mode 100644 index 0000000..841020a --- /dev/null +++ b/backend/app/http/orders/service.go @@ -0,0 +1,77 @@ +package orders + +import ( + "context" + "database/sql" + + "backend/app/requests" + "backend/database/models/qvyun_v2/public/model" + "backend/database/models/qvyun_v2/public/table" + "backend/providers/otel" + + . "github.com/go-jet/jet/v2/postgres" + log "github.com/sirupsen/logrus" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" +) + +// @provider:except +type Service struct { + db *sql.DB + log *log.Entry `inject:"false"` +} + +func (svc *Service) Prepare() error { + svc.log = log.WithField("module", "orders.service") + _ = Int(1) + return nil +} + +// GetUserOrders +func (svc *Service) GetOrders(ctx context.Context, pagination *requests.Pagination, filter *UserOrderFilter) ([]model.Orders, int64, error) { + _, span := otel.Start(ctx, "users.service.GetUserOrders") + 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.Orders + + cond := tbl.UserID.EQ(Int64(filter.UserID)) + if filter.Status != nil { + cond = cond.AND(tbl.Status.EQ(Int16(int16(*filter.Status)))) + } + + if filter.Type != nil { + cond = cond.AND(tbl.Type.EQ(Int16(int16(*filter.Type)))) + } + + if filter.CreatedAt != nil { + cond = cond.AND(tbl.CreatedAt.LT_EQ(TimestampT(*filter.CreatedAt))) + } + + cntStmt := tbl.SELECT(COUNT(tbl.ID).AS("cnt")).WHERE(cond) + var count struct { + Cnt int64 + } + + if err := cntStmt.QueryContext(ctx, svc.db, &count); err != nil { + return nil, 0, err + } + + stmt := tbl. + SELECT(tbl.AllColumns). + WHERE(cond). + ORDER_BY(tbl.CreatedAt.DESC()). + LIMIT(pagination.Limit). + OFFSET(pagination.Offset()) + span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql())) + + var orders []model.Orders + if err := stmt.QueryContext(ctx, svc.db, &orders); err != nil { + return nil, 0, err + } + return orders, count.Cnt, nil +} diff --git a/backend/app/http/orders/service_test.go b/backend/app/http/orders/service_test.go new file mode 100644 index 0000000..33b9ce0 --- /dev/null +++ b/backend/app/http/orders/service_test.go @@ -0,0 +1,37 @@ +package orders + +import ( + "testing" + + "backend/app/service/testx" + + . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/suite" + "go.uber.org/dig" +) + +type ServiceInjectParams struct { + dig.In + Svc *Service +} + +type ServiceTestSuite struct { + suite.Suite + ServiceInjectParams +} + +func Test_DiscoverMedias(t *testing.T) { + providers := testx.Default().With( + Provide, + ) + + testx.Serve(providers, t, func(params ServiceInjectParams) { + suite.Run(t, &ServiceTestSuite{ServiceInjectParams: params}) + }) +} + +func (s *ServiceTestSuite) Test_Service() { + Convey("Test Service", s.T(), func() { + So(s.Svc, ShouldNotBeNil) + }) +} diff --git a/backend/app/http/tenants/controller.go b/backend/app/http/tenants/controller.go index 7250e9c..74d126b 100644 --- a/backend/app/http/tenants/controller.go +++ b/backend/app/http/tenants/controller.go @@ -19,24 +19,10 @@ func (c *Controller) Prepare() error { return nil } -// Test godoc -// -// @Summary Test -// @Description Test -// @Tags Test -// @Accept json -// @Produce json -// @Param id path int true "AccountID" -// @Param queryFilter query dto.AlarmListQuery true "AlarmListQueryFilter" -// @Param pageFilter query common.PageQueryFilter true "PageQueryFilter" -// @Param sortFilter query common.SortQueryFilter true "SortQueryFilter" -// @Success 200 {object} common.PageDataResponse{list=dto.AlarmItem} -// @Router /v1/test/:id [get] - // @Router /t/:tenant [get] // // @Bind claim local -// @Bind tenant query +// @Bind tenant path func (c *Controller) Index(ctx fiber.Ctx, tenant string, claim *jwt.Claims) error { _ctx, span := otel.Start(ctx.Context(), "tenants.controller.index") defer span.End() @@ -50,5 +36,6 @@ func (c *Controller) Index(ctx fiber.Ctx, tenant string, claim *jwt.Claims) erro return err } + // TODO: render page return nil } diff --git a/backend/app/http/tenants/routes.gen.go b/backend/app/http/tenants/routes.gen.go index 8ee87cd..b629a72 100644 --- a/backend/app/http/tenants/routes.gen.go +++ b/backend/app/http/tenants/routes.gen.go @@ -31,7 +31,7 @@ func (r *Routes) Register(router fiber.Router) { // 注册路由组: Controller router.Get("/t/:tenant", Func2( r.controller.Index, - QueryParam[string]("tenant"), + PathParam[string]("tenant"), Local[*jwt.Claims]("claim"), )) diff --git a/backend/app/http/users/controller.go b/backend/app/http/users/controller.go index 88add18..a1d4c7c 100644 --- a/backend/app/http/users/controller.go +++ b/backend/app/http/users/controller.go @@ -1,6 +1,10 @@ package users import ( + "backend/providers/jwt" + + "github.com/gofiber/fiber/v3" + "github.com/jinzhu/copier" log "github.com/sirupsen/logrus" ) @@ -15,16 +19,33 @@ func (c *Controller) Prepare() error { return nil } -// Test godoc +// Show user info // -// @Summary Test -// @Description Test -// @Tags Test -// @Accept json -// @Produce json -// @Param id path int true "AccountID" -// @Param queryFilter query dto.AlarmListQuery true "AlarmListQueryFilter" -// @Param pageFilter query common.PageQueryFilter true "PageQueryFilter" -// @Param sortFilter query common.SortQueryFilter true "SortQueryFilter" -// @Success 200 {object} common.PageDataResponse{list=dto.AlarmItem} -// @Router /v1/test/:id [get] +// @Router /api/v1/users/info [get] +// @Bind claim local +func (c *Controller) Info(ctx fiber.Ctx, claim *jwt.Claims) (*UserInfo, error) { + userInfo := &UserInfo{} + + user, err := c.svc.GetUserByID(ctx.Context(), claim.UserID) + if err != nil { + return nil, err + } + + if err := copier.Copy(userInfo, user); err != nil { + return nil, err + } + + userAuths, err := c.svc.GetUserOAuthChannels(ctx.Context(), claim.UserID) + if err != nil { + return nil, err + } + + for _, auth := range userAuths { + userInfo.OAuths = append(userInfo.OAuths, OAuth{ + Channel: auth.Channel, + ExpireAt: auth.ExpireAt, + }) + } + + return userInfo, nil +} diff --git a/backend/app/http/users/dto.go b/backend/app/http/users/dto.go index 82abcb9..78809df 100644 --- a/backend/app/http/users/dto.go +++ b/backend/app/http/users/dto.go @@ -1 +1,26 @@ package users + +import ( + "time" + + "backend/database/fields" +) + +type UserInfo struct { + ID int64 `json:"id"` + CreatedAt time.Time `json:"created_at"` + Status fields.UserStatus `json:"status"` + Email string `json:"email"` + Phone string `json:"phone"` + Nickname *string `json:"nickname"` + Username string `json:"username"` + Age int16 `json:"age"` + Sex int16 `json:"sex"` + Avatar *string `json:"avatar"` + OAuths []OAuth `json:"oauths"` +} + +type OAuth struct { + Channel fields.AuthChannel `json:"channel"` + ExpireAt time.Time `json:"expire_at"` +} diff --git a/backend/app/http/users/provider.gen.go b/backend/app/http/users/provider.gen.go index a6effcf..899a96a 100755 --- a/backend/app/http/users/provider.gen.go +++ b/backend/app/http/users/provider.gen.go @@ -5,7 +5,9 @@ import ( "backend/providers/event" + "git.ipao.vip/rogeecn/atom" "git.ipao.vip/rogeecn/atom/container" + "git.ipao.vip/rogeecn/atom/contracts" "git.ipao.vip/rogeecn/atom/utils/opt" ) @@ -24,6 +26,20 @@ func Provide(opts ...opt.Option) error { }); err != nil { return err } + if err := container.Container.Provide(func( + controller *Controller, + ) (contracts.HttpRoute, error) { + obj := &Routes{ + controller: controller, + } + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }, atom.GroupRoutes); err != nil { + return err + } if err := container.Container.Provide(func( db *sql.DB, event *event.PubSub, diff --git a/backend/app/http/users/routes.gen.go b/backend/app/http/users/routes.gen.go new file mode 100644 index 0000000..5ae7d6d --- /dev/null +++ b/backend/app/http/users/routes.gen.go @@ -0,0 +1,37 @@ +// Code generated by the atomctl ; DO NOT EDIT. + +package users + +import ( + . "backend/pkg/f" + "backend/providers/jwt" + + _ "git.ipao.vip/rogeecn/atom" + _ "git.ipao.vip/rogeecn/atom/contracts" + "github.com/gofiber/fiber/v3" + log "github.com/sirupsen/logrus" +) + +// @provider contracts.HttpRoute atom.GroupRoutes +type Routes struct { + log *log.Entry `inject:"false"` + controller *Controller +} + +func (r *Routes) Prepare() error { + r.log = log.WithField("module", "routes.users") + return nil +} + +func (r *Routes) Name() string { + return "users" +} + +func (r *Routes) Register(router fiber.Router) { + // 注册路由组: Controller + router.Get("/users/info", DataFunc1( + r.controller.Info, + Local[*jwt.Claims]("claim"), + )) + +} diff --git a/backend/app/http/users/service.go b/backend/app/http/users/service.go index 7c457af..88d00b1 100644 --- a/backend/app/http/users/service.go +++ b/backend/app/http/users/service.go @@ -228,3 +228,29 @@ func (svc *Service) SetUserStatusByID(ctx context.Context, userID int64, status } return nil } + +// GetUserOAuthChannels +func (svc *Service) GetUserOAuthChannels(ctx context.Context, userID int64) ([]model.UserOauths, error) { + _, span := otel.Start(ctx, "users.service.GetUserOAuthChannels") + defer span.End() + span.SetAttributes( + attribute.Int64("user.id", userID), + ) + + tbl := table.UserOauths + stmt := tbl. + SELECT( + tbl.Channel, + tbl.ExpireAt, + ). + WHERE( + tbl.UserID.EQ(Int64(userID)), + ) + span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql())) + + var oauths []model.UserOauths + if err := stmt.QueryContext(ctx, svc.db, &oauths); err != nil { + return nil, err + } + return oauths, nil +} diff --git a/backend/app/requests/pagination.go b/backend/app/requests/pagination.go index 31bb855..af0cdef 100644 --- a/backend/app/requests/pagination.go +++ b/backend/app/requests/pagination.go @@ -9,11 +9,11 @@ type Pager struct { } type Pagination struct { - Page int `json:"page" form:"page" query:"page"` - Limit int `json:"limit" form:"limit" query:"limit"` + Page int64 `json:"page" form:"page" query:"page"` + Limit int64 `json:"limit" form:"limit" query:"limit"` } -func (filter *Pagination) Offset() int { +func (filter *Pagination) Offset() int64 { return (filter.Page - 1) * filter.Limit } @@ -22,7 +22,7 @@ func (filter *Pagination) Format() *Pagination { filter.Page = 1 } - if !lo.Contains([]int{10, 20, 50, 100}, filter.Limit) { + if !lo.Contains([]int64{10, 20, 50, 100}, filter.Limit) { filter.Limit = 10 } diff --git a/backend/go.mod b/backend/go.mod index 1ba0ef8..9a2f68d 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -16,6 +16,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 github.com/imroc/req/v3 v3.49.1 github.com/jackc/pgx/v5 v5.7.2 + github.com/jinzhu/copier v0.4.0 github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a github.com/lib/pq v1.10.9 github.com/opentracing/opentracing-go v1.2.0 diff --git a/backend/go.sum b/backend/go.sum index 06abd7a..ca0b15c 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -166,6 +166,8 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= diff --git a/backend/pkg/f/bind.go b/backend/pkg/f/bind.go index 5a3713a..308c13f 100644 --- a/backend/pkg/f/bind.go +++ b/backend/pkg/f/bind.go @@ -19,14 +19,10 @@ func Path[T fiber.GenericType](key string) func(fiber.Ctx) (T, error) { } } -func URI[T any](name string) func(fiber.Ctx) (*T, error) { - return func(ctx fiber.Ctx) (*T, error) { - p := new(T) - if err := ctx.Bind().URI(p); err != nil { - return nil, errors.Wrapf(err, "uri: %s", name) - } - - return p, nil +func PathParam[T fiber.GenericType](name string) func(fiber.Ctx) (T, error) { + return func(ctx fiber.Ctx) (T, error) { + v := fiber.Params[T](ctx, name) + return v, nil } } @@ -70,3 +66,20 @@ func Header[T any](name string) func(fiber.Ctx) (*T, error) { return p, nil } } + +func Cookie[T any](name string) func(fiber.Ctx) (*T, error) { + return func(ctx fiber.Ctx) (*T, error) { + p := new(T) + if err := ctx.Bind().Cookie(p); err != nil { + return nil, errors.Wrapf(err, "cookie: %s", name) + } + + return p, nil + } +} + +func CookieParam(name string) func(fiber.Ctx) (string, error) { + return func(ctx fiber.Ctx) (string, error) { + return ctx.Cookies(name), nil + } +}