From 8baab461320ff1d66d3a0a44cee060cabec62337 Mon Sep 17 00:00:00 2001 From: yanghao05 Date: Thu, 10 Apr 2025 21:42:13 +0800 Subject: [PATCH] feat: add orders --- backend/app/http/admin/orders.go | 25 ++ backend/app/http/admin/provider.gen.go | 9 + backend/app/http/admin/routes.gen.go | 8 + backend/app/models/models.gen.go | 3 + backend/app/models/orders.go | 168 ++++++++++++ backend/app/models/orders_test.go | 47 ++++ backend/app/models/provider.gen.go | 12 + backend/database/fields/orders.gen.go | 259 ++++++++++++++++++ backend/database/fields/orders.go | 7 + .../20250410130530_create_orders.sql | 26 ++ .../database/schemas/public/model/orders.go | 31 +++ .../database/schemas/public/table/orders.go | 117 ++++++++ .../schemas/public/table/table_use_schema.go | 1 + backend/database/transform.yaml | 6 +- backend/test.http | 4 + frontend/admin/src/App.vue | 5 + frontend/admin/src/api/orderService.js | 16 ++ frontend/admin/src/api/order_list.json | 24 ++ frontend/admin/src/pages/OrderPage.vue | 207 ++++++++++++++ frontend/admin/src/router.js | 5 + 20 files changed, 979 insertions(+), 1 deletion(-) create mode 100644 backend/app/http/admin/orders.go create mode 100644 backend/app/models/orders.go create mode 100644 backend/app/models/orders_test.go create mode 100644 backend/database/fields/orders.gen.go create mode 100644 backend/database/fields/orders.go create mode 100644 backend/database/migrations/20250410130530_create_orders.sql create mode 100644 backend/database/schemas/public/model/orders.go create mode 100644 backend/database/schemas/public/table/orders.go create mode 100644 frontend/admin/src/api/orderService.js create mode 100644 frontend/admin/src/api/order_list.json create mode 100644 frontend/admin/src/pages/OrderPage.vue diff --git a/backend/app/http/admin/orders.go b/backend/app/http/admin/orders.go new file mode 100644 index 0000000..f82115e --- /dev/null +++ b/backend/app/http/admin/orders.go @@ -0,0 +1,25 @@ +package admin + +import ( + "quyun/app/models" + "quyun/app/requests" + + "github.com/gofiber/fiber/v3" +) + +type OrderListQuery struct { + OrderNumber *string `query:"order_number"` + UserID *int64 `query:"user_id"` +} + +// @provider +type orders struct{} + +// List users +// @Router /v1/admin/orders [get] +// @Bind pagination query +// @Bind query query +func (ctl *orders) List(ctx fiber.Ctx, pagination *requests.Pagination, query *OrderListQuery) (*requests.Pager, error) { + cond := models.Orders.BuildConditionWithKey(query.OrderNumber, query.UserID) + return models.Orders.List(ctx.Context(), pagination, cond) +} diff --git a/backend/app/http/admin/provider.gen.go b/backend/app/http/admin/provider.gen.go index b967710..b860552 100755 --- a/backend/app/http/admin/provider.gen.go +++ b/backend/app/http/admin/provider.gen.go @@ -18,6 +18,13 @@ func Provide(opts ...opt.Option) error { }); err != nil { return err } + if err := container.Container.Provide(func() (*orders, error) { + obj := &orders{} + + return obj, nil + }); err != nil { + return err + } if err := container.Container.Provide(func() (*posts, error) { obj := &posts{} @@ -27,12 +34,14 @@ func Provide(opts ...opt.Option) error { } if err := container.Container.Provide(func( medias *medias, + orders *orders, posts *posts, uploads *uploads, users *users, ) (contracts.HttpRoute, error) { obj := &Routes{ medias: medias, + orders: orders, posts: posts, uploads: uploads, users: users, diff --git a/backend/app/http/admin/routes.gen.go b/backend/app/http/admin/routes.gen.go index b983a64..d74bc37 100644 --- a/backend/app/http/admin/routes.gen.go +++ b/backend/app/http/admin/routes.gen.go @@ -16,6 +16,7 @@ import ( type Routes struct { log *log.Entry `inject:"false"` medias *medias + orders *orders posts *posts uploads *uploads users *users @@ -38,6 +39,13 @@ func (r *Routes) Register(router fiber.Router) { Query[ListQuery]("query"), )) + // 注册路由组: orders + router.Get("/v1/admin/orders", DataFunc2( + r.orders.List, + Query[requests.Pagination]("pagination"), + Query[OrderListQuery]("query"), + )) + // 注册路由组: posts router.Get("/v1/admin/posts", DataFunc2( r.posts.List, diff --git a/backend/app/models/models.gen.go b/backend/app/models/models.gen.go index f317c78..ca66481 100644 --- a/backend/app/models/models.gen.go +++ b/backend/app/models/models.gen.go @@ -9,6 +9,7 @@ import ( var db *sql.DB var Medias *mediasModel +var Orders *ordersModel var Posts *postsModel var Users *usersModel @@ -16,6 +17,7 @@ var Users *usersModel type models struct { db *sql.DB medias *mediasModel + orders *ordersModel posts *postsModel users *usersModel } @@ -23,6 +25,7 @@ type models struct { func (m *models) Prepare() error { db = m.db Medias = m.medias + Orders = m.orders Posts = m.posts Users = m.users return nil diff --git a/backend/app/models/orders.go b/backend/app/models/orders.go new file mode 100644 index 0000000..4446e57 --- /dev/null +++ b/backend/app/models/orders.go @@ -0,0 +1,168 @@ +package models + +import ( + "context" + "fmt" + "time" + + "quyun/app/requests" + "quyun/database/fields" + "quyun/database/schemas/public/model" + "quyun/database/schemas/public/table" + + . "github.com/go-jet/jet/v2/postgres" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// @provider +type ordersModel struct { + log *logrus.Entry `inject:"false"` +} + +func (m *ordersModel) Prepare() error { + m.log = logrus.WithField("model", "ordersModel") + return nil +} + +// GetByID returns an order by ID +func (m *ordersModel) GetByID(ctx context.Context, id int64) (*model.Orders, error) { + tbl := table.Orders + + stmt := tbl. + SELECT(tbl.AllColumns). + WHERE( + tbl.ID.EQ(Int64(id)), + ) + m.log.Infof("sql: %s", stmt.DebugSql()) + + var order model.Orders + err := stmt.QueryContext(ctx, db, &order) + if err != nil { + m.log.Errorf("error querying order by ID: %v", err) + return nil, err + } + + return &order, nil +} + +// BuildConditionWithKey builds the WHERE clause for order queries +func (m *ordersModel) BuildConditionWithKey(orderNumber *string, userID *int64) BoolExpression { + tbl := table.Orders + + cond := Bool(true) + + if orderNumber != nil && *orderNumber != "" { + cond = cond.AND( + tbl.OrderNo.LIKE(String("%" + *orderNumber + "%")), + ) + } + + if userID != nil { + cond = cond.AND( + tbl.UserID.EQ(Int(*userID)), + ) + } + + return cond +} + +// countByCondition counts orders matching the given condition +func (m *ordersModel) countByCondition(ctx context.Context, expr BoolExpression) (int64, error) { + var cnt struct { + Cnt int64 + } + + tbl := table.Orders + stmt := SELECT(COUNT(tbl.ID).AS("cnt")).FROM(tbl).WHERE(expr) + m.log.Infof("sql: %s", stmt.DebugSql()) + + err := stmt.QueryContext(ctx, db, &cnt) + if err != nil { + m.log.Errorf("error counting orders: %v", err) + return 0, err + } + + return cnt.Cnt, nil +} + +// List returns a paginated list of orders +func (m *ordersModel) List(ctx context.Context, pagination *requests.Pagination, cond BoolExpression) (*requests.Pager, error) { + pagination.Format() + + tbl := table.Orders + stmt := tbl. + SELECT(tbl.AllColumns). + WHERE(cond). + ORDER_BY(tbl.ID.DESC()). + LIMIT(pagination.Limit). + OFFSET(pagination.Offset) + m.log.Infof("sql: %s", stmt.DebugSql()) + + var orders []model.Orders = make([]model.Orders, 0) + err := stmt.QueryContext(ctx, db, &orders) + if err != nil { + m.log.Errorf("error querying orders: %v", err) + return nil, err + } + + count, err := m.countByCondition(ctx, cond) + if err != nil { + m.log.Errorf("error getting order count: %v", err) + return nil, err + } + + return &requests.Pager{ + Items: orders, + Total: count, + Pagination: *pagination, + }, nil +} + +// Create creates a new order +func (m *ordersModel) Create(ctx context.Context, userId, postId int64) (*model.Orders, error) { + post, err := Posts.GetByID(ctx, postId) + if err != nil { + return nil, errors.Wrap(err, "failed to get post") + } + + model := &model.Orders{} + model.CreatedAt = time.Now() + model.UpdatedAt = time.Now() + model.Status = fields.OrderStatusPending + model.OrderNo = fmt.Sprintf("%s", time.Now().Format("20060102150405")) + model.SubOrderNo = model.OrderNo + model.UserID = userId + model.PostID = postId + model.Meta = fields.ToJson(fields.OrderMeta{}) + model.Price = post.Price + model.Discount = post.Discount + + tbl := table.Orders + stmt := tbl.INSERT(tbl.MutableColumns).MODEL(model) + m.log.Infof("sql: %s", stmt.DebugSql()) + + if _, err := stmt.ExecContext(ctx, db); err != nil { + m.log.Errorf("error creating order: %v", err) + return nil, err + } + return model, nil +} + +// DeleteByID soft deletes an order by ID +func (m *ordersModel) SetStatus(ctx context.Context, orderNo string, status fields.OrderStatus) error { + tbl := table.Orders + stmt := tbl. + UPDATE(tbl.Status). + SET(status). + WHERE( + tbl.OrderNo.EQ(String(orderNo)), + ) + m.log.Infof("sql: %s", stmt.DebugSql()) + + if _, err := stmt.ExecContext(ctx, db); err != nil { + m.log.Errorf("error set order status: %v", err) + return err + } + return nil +} diff --git a/backend/app/models/orders_test.go b/backend/app/models/orders_test.go new file mode 100644 index 0000000..853d373 --- /dev/null +++ b/backend/app/models/orders_test.go @@ -0,0 +1,47 @@ +package models + +import ( + "context" + "testing" + + "quyun/app/service/testx" + "quyun/database" + "quyun/database/schemas/public/table" + + . "github.com/smartystreets/goconvey/convey" + "go.ipao.vip/atom/contracts" + + // . "github.com/go-jet/jet/v2/postgres" + "github.com/stretchr/testify/suite" + "go.uber.org/dig" +) + +type OrdersInjectParams struct { + dig.In + Initials []contracts.Initial `group:"initials"` +} + +type OrdersTestSuite struct { + suite.Suite + + OrdersInjectParams +} + +func Test_Orders(t *testing.T) { + providers := testx.Default().With(Provide) + testx.Serve(providers, t, func(params OrdersInjectParams) { + suite.Run(t, &OrdersTestSuite{ + OrdersInjectParams: params, + }) + }) +} + +func (s *OrdersTestSuite) Test_Create() { + Convey("Test_Create", s.T(), func() { + database.Truncate(context.Background(), db, table.Orders.TableName()) + + order, err := Orders.Create(context.Background(), 1, 1) + So(err, ShouldBeNil) + s.T().Logf("order: %v", order) + }) +} diff --git a/backend/app/models/provider.gen.go b/backend/app/models/provider.gen.go index 4e7e7ed..80fbd4b 100755 --- a/backend/app/models/provider.gen.go +++ b/backend/app/models/provider.gen.go @@ -23,12 +23,14 @@ func Provide(opts ...opt.Option) error { if err := container.Container.Provide(func( db *sql.DB, medias *mediasModel, + orders *ordersModel, posts *postsModel, users *usersModel, ) (contracts.Initial, error) { obj := &models{ db: db, medias: medias, + orders: orders, posts: posts, users: users, } @@ -40,6 +42,16 @@ func Provide(opts ...opt.Option) error { }, atom.GroupInitial); err != nil { return err } + if err := container.Container.Provide(func() (*ordersModel, error) { + obj := &ordersModel{} + if err := obj.Prepare(); err != nil { + return nil, err + } + + return obj, nil + }); err != nil { + return err + } if err := container.Container.Provide(func() (*postsModel, error) { obj := &postsModel{} if err := obj.Prepare(); err != nil { diff --git a/backend/database/fields/orders.gen.go b/backend/database/fields/orders.gen.go new file mode 100644 index 0000000..1a49ed0 --- /dev/null +++ b/backend/database/fields/orders.gen.go @@ -0,0 +1,259 @@ +// 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 + // OrderStatusCompleted is a OrderStatus of type Completed. + OrderStatusCompleted +) + +var ErrInvalidOrderStatus = fmt.Errorf("not a valid OrderStatus, try [%s]", strings.Join(_OrderStatusNames, ", ")) + +const _OrderStatusName = "pendingpaidrefundingrefundedcancelledcompleted" + +var _OrderStatusNames = []string{ + _OrderStatusName[0:7], + _OrderStatusName[7:11], + _OrderStatusName[11:20], + _OrderStatusName[20:28], + _OrderStatusName[28:37], + _OrderStatusName[37:46], +} + +// 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, + OrderStatusCompleted, + } +} + +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], + OrderStatusCompleted: _OrderStatusName[37:46], +} + +// 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, + _OrderStatusName[37:46]: OrderStatusCompleted, +} + +// 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 +} diff --git a/backend/database/fields/orders.go b/backend/database/fields/orders.go new file mode 100644 index 0000000..6ed3bc6 --- /dev/null +++ b/backend/database/fields/orders.go @@ -0,0 +1,7 @@ +package fields + +// swagger:enum OrderStatus +// ENUM( pending, paid, refunding, refunded, cancelled, completed) +type OrderStatus int16 + +type OrderMeta struct{} diff --git a/backend/database/migrations/20250410130530_create_orders.sql b/backend/database/migrations/20250410130530_create_orders.sql new file mode 100644 index 0000000..c3a5a20 --- /dev/null +++ b/backend/database/migrations/20250410130530_create_orders.sql @@ -0,0 +1,26 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE orders( + id SERIAL8 PRIMARY KEY, + created_at timestamp NOT NULL DEFAULT now(), + updated_at timestamp NOT NULL DEFAULT now(), + order_no varchar(64) NOT NULL, + sub_order_no varchar(64) NOT NULL DEFAULT '', + transaction_id varchar(64) NOT NULL DEFAULT '', + refund_transaction_id varchar(64) NOT NULL DEFAULT '', + price int8 NOT NULL DEFAULT 0, + discount int2 NOT NULL DEFAULT 100, + currency varchar(10) NOT NULL DEFAULT 'CNY', + payment_method varchar(50) NOT NULL DEFAULT 'wechatpay', + post_id int8 NOT NULL, + user_id int8 NOT NULL, + status int2 NOT NULL, + meta jsonb NOT NULL DEFAULT '{}' ::jsonb +); + +-- +goose StatementEnd +-- +goose Down +-- +goose StatementBegin +DROP TABLE orders; + +-- +goose StatementEnd diff --git a/backend/database/schemas/public/model/orders.go b/backend/database/schemas/public/model/orders.go new file mode 100644 index 0000000..064498e --- /dev/null +++ b/backend/database/schemas/public/model/orders.go @@ -0,0 +1,31 @@ +// +// 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 ( + "quyun/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"` + OrderNo string `json:"order_no"` + SubOrderNo string `json:"sub_order_no"` + TransactionID string `json:"transaction_id"` + RefundTransactionID string `json:"refund_transaction_id"` + Price int64 `json:"price"` + Discount int16 `json:"discount"` + Currency string `json:"currency"` + PaymentMethod string `json:"payment_method"` + PostID int64 `json:"post_id"` + UserID int64 `json:"user_id"` + Status fields.OrderStatus `json:"status"` + Meta fields.Json[fields.OrderMeta] `json:"meta"` +} diff --git a/backend/database/schemas/public/table/orders.go b/backend/database/schemas/public/table/orders.go new file mode 100644 index 0000000..7777f78 --- /dev/null +++ b/backend/database/schemas/public/table/orders.go @@ -0,0 +1,117 @@ +// +// 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 Orders = newOrdersTable("public", "orders", "") + +type ordersTable struct { + postgres.Table + + // Columns + ID postgres.ColumnInteger + CreatedAt postgres.ColumnTimestamp + UpdatedAt postgres.ColumnTimestamp + OrderNo postgres.ColumnString + SubOrderNo postgres.ColumnString + TransactionID postgres.ColumnString + RefundTransactionID postgres.ColumnString + Price postgres.ColumnInteger + Discount postgres.ColumnInteger + Currency postgres.ColumnString + PaymentMethod postgres.ColumnString + PostID postgres.ColumnInteger + UserID postgres.ColumnInteger + Status postgres.ColumnInteger + Meta postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type OrdersTable struct { + ordersTable + + EXCLUDED ordersTable +} + +// AS creates new OrdersTable with assigned alias +func (a OrdersTable) AS(alias string) *OrdersTable { + return newOrdersTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new OrdersTable with assigned schema name +func (a OrdersTable) FromSchema(schemaName string) *OrdersTable { + return newOrdersTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new OrdersTable with assigned table prefix +func (a OrdersTable) WithPrefix(prefix string) *OrdersTable { + return newOrdersTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new OrdersTable with assigned table suffix +func (a OrdersTable) WithSuffix(suffix string) *OrdersTable { + return newOrdersTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newOrdersTable(schemaName, tableName, alias string) *OrdersTable { + return &OrdersTable{ + ordersTable: newOrdersTableImpl(schemaName, tableName, alias), + EXCLUDED: newOrdersTableImpl("", "excluded", ""), + } +} + +func newOrdersTableImpl(schemaName, tableName, alias string) ordersTable { + var ( + IDColumn = postgres.IntegerColumn("id") + CreatedAtColumn = postgres.TimestampColumn("created_at") + UpdatedAtColumn = postgres.TimestampColumn("updated_at") + OrderNoColumn = postgres.StringColumn("order_no") + SubOrderNoColumn = postgres.StringColumn("sub_order_no") + TransactionIDColumn = postgres.StringColumn("transaction_id") + RefundTransactionIDColumn = postgres.StringColumn("refund_transaction_id") + PriceColumn = postgres.IntegerColumn("price") + DiscountColumn = postgres.IntegerColumn("discount") + CurrencyColumn = postgres.StringColumn("currency") + PaymentMethodColumn = postgres.StringColumn("payment_method") + PostIDColumn = postgres.IntegerColumn("post_id") + UserIDColumn = postgres.IntegerColumn("user_id") + StatusColumn = postgres.IntegerColumn("status") + MetaColumn = postgres.StringColumn("meta") + allColumns = postgres.ColumnList{IDColumn, CreatedAtColumn, UpdatedAtColumn, OrderNoColumn, SubOrderNoColumn, TransactionIDColumn, RefundTransactionIDColumn, PriceColumn, DiscountColumn, CurrencyColumn, PaymentMethodColumn, PostIDColumn, UserIDColumn, StatusColumn, MetaColumn} + mutableColumns = postgres.ColumnList{CreatedAtColumn, UpdatedAtColumn, OrderNoColumn, SubOrderNoColumn, TransactionIDColumn, RefundTransactionIDColumn, PriceColumn, DiscountColumn, CurrencyColumn, PaymentMethodColumn, PostIDColumn, UserIDColumn, StatusColumn, MetaColumn} + ) + + return ordersTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + ID: IDColumn, + CreatedAt: CreatedAtColumn, + UpdatedAt: UpdatedAtColumn, + OrderNo: OrderNoColumn, + SubOrderNo: SubOrderNoColumn, + TransactionID: TransactionIDColumn, + RefundTransactionID: RefundTransactionIDColumn, + Price: PriceColumn, + Discount: DiscountColumn, + Currency: CurrencyColumn, + PaymentMethod: PaymentMethodColumn, + PostID: PostIDColumn, + UserID: UserIDColumn, + Status: StatusColumn, + Meta: MetaColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/database/schemas/public/table/table_use_schema.go b/backend/database/schemas/public/table/table_use_schema.go index ab5c81a..e8f6717 100644 --- a/backend/database/schemas/public/table/table_use_schema.go +++ b/backend/database/schemas/public/table/table_use_schema.go @@ -12,6 +12,7 @@ package table func UseSchema(schema string) { Medias = Medias.FromSchema(schema) Migrations = Migrations.FromSchema(schema) + Orders = Orders.FromSchema(schema) Posts = Posts.FromSchema(schema) UserPosts = UserPosts.FromSchema(schema) Users = Users.FromSchema(schema) diff --git a/backend/database/transform.yaml b/backend/database/transform.yaml index 810a7b2..49410fc 100644 --- a/backend/database/transform.yaml +++ b/backend/database/transform.yaml @@ -16,7 +16,11 @@ types: status: PostStatus assets: Json[[]MediaAsset] tags: Json[[]string] - meta: PostMeta + meta: Json[PostMeta] users: status: UserStatus + + orders: + status: OrderStatus + meta: Json[OrderMeta] diff --git a/backend/test.http b/backend/test.http index ae3a6db..c6b344a 100644 --- a/backend/test.http +++ b/backend/test.http @@ -50,4 +50,8 @@ DELETE {{host}}/v1/admin/posts/103 HTTP/1.1 ### get users GET {{host}}/v1/admin/users HTTP/1.1 +Content-Type: application/json + +### get orders +GET {{host}}/v1/admin/orders HTTP/1.1 Content-Type: application/json \ No newline at end of file diff --git a/frontend/admin/src/App.vue b/frontend/admin/src/App.vue index 6de9ba3..bf420fd 100644 --- a/frontend/admin/src/App.vue +++ b/frontend/admin/src/App.vue @@ -27,6 +27,11 @@ const navItems = ref([ icon: 'pi pi-users', command: () => router.push('/users') }, + { + label: 'Orders', + icon: 'pi pi-shopping-cart', + command: () => router.push('/orders') + }, { label: 'Settings', icon: 'pi pi-cog', diff --git a/frontend/admin/src/api/orderService.js b/frontend/admin/src/api/orderService.js new file mode 100644 index 0000000..1631fe3 --- /dev/null +++ b/frontend/admin/src/api/orderService.js @@ -0,0 +1,16 @@ +import httpClient from './httpClient'; + +export const orderService = { + getOrders({ page = 1, limit = 10, keyword = '' } = {}) { + return httpClient.get('/admin/orders', { + params: { + page, + limit, + keyword: keyword.trim() + } + }); + }, + deleteOrder(id) { + return httpClient.delete(`/admin/orders/${id}`); + } +} \ No newline at end of file diff --git a/frontend/admin/src/api/order_list.json b/frontend/admin/src/api/order_list.json new file mode 100644 index 0000000..8a8f2ce --- /dev/null +++ b/frontend/admin/src/api/order_list.json @@ -0,0 +1,24 @@ +{ + "page": 1, + "limit": 10, + "total": 1, + "items": [ + { + "id": 1, + "created_at": "2025-04-10T21:30:27.585874Z", + "updated_at": "2025-04-10T21:30:27.585877Z", + "order_no": "20250410213027", + "sub_order_no": "20250410213027", + "transaction_id": "", + "refund_transaction_id": "", + "price": 325, + "discount": 58, + "currency": "", + "payment_method": "", + "post_id": 1, + "user_id": 1, + "status": 0, + "meta": {} + } + ] +} \ No newline at end of file diff --git a/frontend/admin/src/pages/OrderPage.vue b/frontend/admin/src/pages/OrderPage.vue new file mode 100644 index 0000000..48c5c8d --- /dev/null +++ b/frontend/admin/src/pages/OrderPage.vue @@ -0,0 +1,207 @@ + + + diff --git a/frontend/admin/src/router.js b/frontend/admin/src/router.js index 348cc24..0f6e104 100644 --- a/frontend/admin/src/router.js +++ b/frontend/admin/src/router.js @@ -39,6 +39,11 @@ const routes = [ path: '/users', name: 'Users', component: () => import('./pages/UserPage.vue'), + }, + { + path: '/orders', + name: 'Orders', + component: () => import('./pages/OrderPage.vue'), } ];