diff --git a/backend/app/http/orders/routes.gen.go b/backend/app/http/orders/routes.gen.go index 818637d..4c49429 100644 --- a/backend/app/http/orders/routes.gen.go +++ b/backend/app/http/orders/routes.gen.go @@ -30,7 +30,7 @@ func (r *Routes) Name() string { func (r *Routes) Register(router fiber.Router) { // 注册路由组: Controller - router.Get("/users/orders", DataFunc3( + router.Get("/api/v1/orders", DataFunc3( r.controller.List, Local[*jwt.Claims]("claim"), Query[requests.Pagination]("pagination"), diff --git a/backend/app/http/posts/controller.go b/backend/app/http/posts/controller.go new file mode 100644 index 0000000..191235e --- /dev/null +++ b/backend/app/http/posts/controller.go @@ -0,0 +1,53 @@ +package posts + +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", "posts.Controller") + return nil +} + +// List show posts list +// @Router /api/v1/posts [get] +// @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) { + pagination.Format() + pager := &requests.Pager{ + Pagination: *pagination, + } + + filter.TenantID = *claim.TenantID + filter.UserID = claim.UserID + orders, total, err := c.svc.GetPosts(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 +} diff --git a/backend/app/http/posts/dto.go b/backend/app/http/posts/dto.go new file mode 100644 index 0000000..981ff0e --- /dev/null +++ b/backend/app/http/posts/dto.go @@ -0,0 +1,32 @@ +package posts + +import ( + "time" +) + +type UserPost struct { + ID int64 `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + HashID string `json:"hash_id"` + Title string `json:"title"` + Description string `json:"description"` + Poster string `json:"poster"` + Content string `json:"content"` + Stage int16 `json:"stage"` + Status int16 `json:"status"` + Price int64 `json:"price"` + Discount int16 `json:"discount"` + Views int64 `json:"views"` + Likes int64 `json:"likes"` + Meta *string `json:"meta"` + Assets *string `json:"assets"` +} + +type UserPostFilter struct { + ID *int64 `json:"id"` + TenantID int64 `query:"tenant_id"` + UserID int64 `query:"user_id"` + CreatedAt *time.Time `query:"created_at"` + Keyword *string `json:"title"` +} diff --git a/backend/app/http/posts/provider.gen.go b/backend/app/http/posts/provider.gen.go new file mode 100755 index 0000000..c55c455 --- /dev/null +++ b/backend/app/http/posts/provider.gen.go @@ -0,0 +1,56 @@ +package posts + +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/posts/routes.gen.go b/backend/app/http/posts/routes.gen.go new file mode 100644 index 0000000..d24c559 --- /dev/null +++ b/backend/app/http/posts/routes.gen.go @@ -0,0 +1,40 @@ +// Code generated by the atomctl ; DO NOT EDIT. + +package posts + +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.posts") + return nil +} + +func (r *Routes) Name() string { + return "posts" +} + +func (r *Routes) Register(router fiber.Router) { + // 注册路由组: Controller + router.Get("/api/v1/posts", DataFunc3( + r.controller.List, + Local[*jwt.Claims]("claim"), + Query[requests.Pagination]("pagination"), + Query[UserPostFilter]("filter"), + )) + +} diff --git a/backend/app/http/posts/service.go b/backend/app/http/posts/service.go new file mode 100644 index 0000000..b970461 --- /dev/null +++ b/backend/app/http/posts/service.go @@ -0,0 +1,91 @@ +package posts + +import ( + "context" + "database/sql" + + "backend/app/requests" + "backend/database" + "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", "posts.service") + _ = Int(1) + return 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") + 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 + + cond := Bool(true) + if filter.ID != nil { + cond = cond.AND(tbl.ID.EQ(Int64(*filter.ID))) + } + if filter.UserID != 0 { + cond = cond.AND(tbl.UserID.EQ(Int64(filter.UserID))) + } + if filter.CreatedAt != nil { + cond = cond.AND(tbl.CreatedAt.LT_EQ(TimestampT(*filter.CreatedAt))) + } + if filter.TenantID != 0 { + cond = cond.AND(tbl.TenantID.EQ(Int64(filter.TenantID))) + } + 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 +} diff --git a/backend/app/http/posts/service_test.go b/backend/app/http/posts/service_test.go new file mode 100644 index 0000000..b9927d1 --- /dev/null +++ b/backend/app/http/posts/service_test.go @@ -0,0 +1,37 @@ +package posts + +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/users/routes.gen.go b/backend/app/http/users/routes.gen.go index 5ae7d6d..a4524c6 100644 --- a/backend/app/http/users/routes.gen.go +++ b/backend/app/http/users/routes.gen.go @@ -29,7 +29,7 @@ func (r *Routes) Name() string { func (r *Routes) Register(router fiber.Router) { // 注册路由组: Controller - router.Get("/users/info", DataFunc1( + router.Get("/api/v1/users/info", DataFunc1( r.controller.Info, Local[*jwt.Claims]("claim"), )) diff --git a/backend/app/http/users/service_test.go b/backend/app/http/users/service_test.go index 9daafca..bd36095 100644 --- a/backend/app/http/users/service_test.go +++ b/backend/app/http/users/service_test.go @@ -55,7 +55,6 @@ func (s *ServiceTestSuite) Test_GetUserIDByOpenID() { Channel: fields.AuthChannelWeChat, UserID: 1, OpenID: "test_open_id", - AccessKey: "test_access_key", AccessToken: "test_access_token", RefreshToken: "test_refresh_token", ExpireAt: time.Now().Add(time.Hour), diff --git a/backend/database/fields/storage.gen.go b/backend/database/fields/storage.gen.go new file mode 100644 index 0000000..779c1a5 --- /dev/null +++ b/backend/database/fields/storage.gen.go @@ -0,0 +1,247 @@ +// 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 ( + // StorageTypeLocal is a StorageType of type Local. + StorageTypeLocal StorageType = iota + // StorageTypeAliOSS is a StorageType of type AliOSS. + StorageTypeAliOSS + // StorageTypeS3 is a StorageType of type S3. + StorageTypeS3 + // StorageTypeMinIO is a StorageType of type MinIO. + StorageTypeMinIO +) + +var ErrInvalidStorageType = fmt.Errorf("not a valid StorageType, try [%s]", strings.Join(_StorageTypeNames, ", ")) + +const _StorageTypeName = "LocalAliOSSS3MinIO" + +var _StorageTypeNames = []string{ + _StorageTypeName[0:5], + _StorageTypeName[5:11], + _StorageTypeName[11:13], + _StorageTypeName[13:18], +} + +// StorageTypeNames returns a list of possible string values of StorageType. +func StorageTypeNames() []string { + tmp := make([]string, len(_StorageTypeNames)) + copy(tmp, _StorageTypeNames) + return tmp +} + +// StorageTypeValues returns a list of the values for StorageType +func StorageTypeValues() []StorageType { + return []StorageType{ + StorageTypeLocal, + StorageTypeAliOSS, + StorageTypeS3, + StorageTypeMinIO, + } +} + +var _StorageTypeMap = map[StorageType]string{ + StorageTypeLocal: _StorageTypeName[0:5], + StorageTypeAliOSS: _StorageTypeName[5:11], + StorageTypeS3: _StorageTypeName[11:13], + StorageTypeMinIO: _StorageTypeName[13:18], +} + +// String implements the Stringer interface. +func (x StorageType) String() string { + if str, ok := _StorageTypeMap[x]; ok { + return str + } + return fmt.Sprintf("StorageType(%d)", x) +} + +// IsValid provides a quick way to determine if the typed value is +// part of the allowed enumerated values +func (x StorageType) IsValid() bool { + _, ok := _StorageTypeMap[x] + return ok +} + +var _StorageTypeValue = map[string]StorageType{ + _StorageTypeName[0:5]: StorageTypeLocal, + _StorageTypeName[5:11]: StorageTypeAliOSS, + _StorageTypeName[11:13]: StorageTypeS3, + _StorageTypeName[13:18]: StorageTypeMinIO, +} + +// ParseStorageType attempts to convert a string to a StorageType. +func ParseStorageType(name string) (StorageType, error) { + if x, ok := _StorageTypeValue[name]; ok { + return x, nil + } + return StorageType(0), fmt.Errorf("%s is %w", name, ErrInvalidStorageType) +} + +var errStorageTypeNilPtr = errors.New("value pointer is nil") // one per type for package clashes + +// Scan implements the Scanner interface. +func (x *StorageType) Scan(value interface{}) (err error) { + if value == nil { + *x = StorageType(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 = StorageType(v) + case string: + *x, err = ParseStorageType(v) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(v); verr == nil { + *x, err = StorageType(val), nil + } + } + case []byte: + *x, err = ParseStorageType(string(v)) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(string(v)); verr == nil { + *x, err = StorageType(val), nil + } + } + case StorageType: + *x = v + case int: + *x = StorageType(v) + case *StorageType: + if v == nil { + return errStorageTypeNilPtr + } + *x = *v + case uint: + *x = StorageType(v) + case uint64: + *x = StorageType(v) + case *int: + if v == nil { + return errStorageTypeNilPtr + } + *x = StorageType(*v) + case *int64: + if v == nil { + return errStorageTypeNilPtr + } + *x = StorageType(*v) + case float64: // json marshals everything as a float64 if it's a number + *x = StorageType(v) + case *float64: // json marshals everything as a float64 if it's a number + if v == nil { + return errStorageTypeNilPtr + } + *x = StorageType(*v) + case *uint: + if v == nil { + return errStorageTypeNilPtr + } + *x = StorageType(*v) + case *uint64: + if v == nil { + return errStorageTypeNilPtr + } + *x = StorageType(*v) + case *string: + if v == nil { + return errStorageTypeNilPtr + } + *x, err = ParseStorageType(*v) + if err != nil { + // try parsing the integer value as a string + if val, verr := strconv.Atoi(*v); verr == nil { + *x, err = StorageType(val), nil + } + } + } + + return +} + +// Value implements the driver Valuer interface. +func (x StorageType) Value() (driver.Value, error) { + return int64(x), nil +} + +// Set implements the Golang flag.Value interface func. +func (x *StorageType) Set(val string) error { + v, err := ParseStorageType(val) + *x = v + return err +} + +// Get implements the Golang flag.Getter interface func. +func (x *StorageType) Get() interface{} { + return *x +} + +// Type implements the github.com/spf13/pFlag Value interface. +func (x *StorageType) Type() string { + return "StorageType" +} + +type NullStorageType struct { + StorageType StorageType + Valid bool +} + +func NewNullStorageType(val interface{}) (x NullStorageType) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Scan implements the Scanner interface. +func (x *NullStorageType) Scan(value interface{}) (err error) { + if value == nil { + x.StorageType, x.Valid = StorageType(0), false + return + } + + err = x.StorageType.Scan(value) + x.Valid = (err == nil) + return +} + +// Value implements the driver Valuer interface. +func (x NullStorageType) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + // driver.Value accepts int64 for int values. + return int64(x.StorageType), nil +} + +type NullStorageTypeStr struct { + NullStorageType +} + +func NewNullStorageTypeStr(val interface{}) (x NullStorageTypeStr) { + x.Scan(val) // yes, we ignore this error, it will just be an invalid value. + return +} + +// Value implements the driver Valuer interface. +func (x NullStorageTypeStr) Value() (driver.Value, error) { + if !x.Valid { + return nil, nil + } + return x.StorageType.String(), nil +}