feat: add orders
This commit is contained in:
25
backend/app/http/admin/orders.go
Normal file
25
backend/app/http/admin/orders.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -18,6 +18,13 @@ func Provide(opts ...opt.Option) error {
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
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) {
|
if err := container.Container.Provide(func() (*posts, error) {
|
||||||
obj := &posts{}
|
obj := &posts{}
|
||||||
|
|
||||||
@@ -27,12 +34,14 @@ func Provide(opts ...opt.Option) error {
|
|||||||
}
|
}
|
||||||
if err := container.Container.Provide(func(
|
if err := container.Container.Provide(func(
|
||||||
medias *medias,
|
medias *medias,
|
||||||
|
orders *orders,
|
||||||
posts *posts,
|
posts *posts,
|
||||||
uploads *uploads,
|
uploads *uploads,
|
||||||
users *users,
|
users *users,
|
||||||
) (contracts.HttpRoute, error) {
|
) (contracts.HttpRoute, error) {
|
||||||
obj := &Routes{
|
obj := &Routes{
|
||||||
medias: medias,
|
medias: medias,
|
||||||
|
orders: orders,
|
||||||
posts: posts,
|
posts: posts,
|
||||||
uploads: uploads,
|
uploads: uploads,
|
||||||
users: users,
|
users: users,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
type Routes struct {
|
type Routes struct {
|
||||||
log *log.Entry `inject:"false"`
|
log *log.Entry `inject:"false"`
|
||||||
medias *medias
|
medias *medias
|
||||||
|
orders *orders
|
||||||
posts *posts
|
posts *posts
|
||||||
uploads *uploads
|
uploads *uploads
|
||||||
users *users
|
users *users
|
||||||
@@ -38,6 +39,13 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
Query[ListQuery]("query"),
|
Query[ListQuery]("query"),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
// 注册路由组: orders
|
||||||
|
router.Get("/v1/admin/orders", DataFunc2(
|
||||||
|
r.orders.List,
|
||||||
|
Query[requests.Pagination]("pagination"),
|
||||||
|
Query[OrderListQuery]("query"),
|
||||||
|
))
|
||||||
|
|
||||||
// 注册路由组: posts
|
// 注册路由组: posts
|
||||||
router.Get("/v1/admin/posts", DataFunc2(
|
router.Get("/v1/admin/posts", DataFunc2(
|
||||||
r.posts.List,
|
r.posts.List,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
var db *sql.DB
|
var db *sql.DB
|
||||||
var Medias *mediasModel
|
var Medias *mediasModel
|
||||||
|
var Orders *ordersModel
|
||||||
var Posts *postsModel
|
var Posts *postsModel
|
||||||
var Users *usersModel
|
var Users *usersModel
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ var Users *usersModel
|
|||||||
type models struct {
|
type models struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
medias *mediasModel
|
medias *mediasModel
|
||||||
|
orders *ordersModel
|
||||||
posts *postsModel
|
posts *postsModel
|
||||||
users *usersModel
|
users *usersModel
|
||||||
}
|
}
|
||||||
@@ -23,6 +25,7 @@ type models struct {
|
|||||||
func (m *models) Prepare() error {
|
func (m *models) Prepare() error {
|
||||||
db = m.db
|
db = m.db
|
||||||
Medias = m.medias
|
Medias = m.medias
|
||||||
|
Orders = m.orders
|
||||||
Posts = m.posts
|
Posts = m.posts
|
||||||
Users = m.users
|
Users = m.users
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
168
backend/app/models/orders.go
Normal file
168
backend/app/models/orders.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
47
backend/app/models/orders_test.go
Normal file
47
backend/app/models/orders_test.go
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -23,12 +23,14 @@ func Provide(opts ...opt.Option) error {
|
|||||||
if err := container.Container.Provide(func(
|
if err := container.Container.Provide(func(
|
||||||
db *sql.DB,
|
db *sql.DB,
|
||||||
medias *mediasModel,
|
medias *mediasModel,
|
||||||
|
orders *ordersModel,
|
||||||
posts *postsModel,
|
posts *postsModel,
|
||||||
users *usersModel,
|
users *usersModel,
|
||||||
) (contracts.Initial, error) {
|
) (contracts.Initial, error) {
|
||||||
obj := &models{
|
obj := &models{
|
||||||
db: db,
|
db: db,
|
||||||
medias: medias,
|
medias: medias,
|
||||||
|
orders: orders,
|
||||||
posts: posts,
|
posts: posts,
|
||||||
users: users,
|
users: users,
|
||||||
}
|
}
|
||||||
@@ -40,6 +42,16 @@ func Provide(opts ...opt.Option) error {
|
|||||||
}, atom.GroupInitial); err != nil {
|
}, atom.GroupInitial); err != nil {
|
||||||
return err
|
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) {
|
if err := container.Container.Provide(func() (*postsModel, error) {
|
||||||
obj := &postsModel{}
|
obj := &postsModel{}
|
||||||
if err := obj.Prepare(); err != nil {
|
if err := obj.Prepare(); err != nil {
|
||||||
|
|||||||
259
backend/database/fields/orders.gen.go
Normal file
259
backend/database/fields/orders.gen.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
7
backend/database/fields/orders.go
Normal file
7
backend/database/fields/orders.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package fields
|
||||||
|
|
||||||
|
// swagger:enum OrderStatus
|
||||||
|
// ENUM( pending, paid, refunding, refunded, cancelled, completed)
|
||||||
|
type OrderStatus int16
|
||||||
|
|
||||||
|
type OrderMeta struct{}
|
||||||
26
backend/database/migrations/20250410130530_create_orders.sql
Normal file
26
backend/database/migrations/20250410130530_create_orders.sql
Normal file
@@ -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
|
||||||
31
backend/database/schemas/public/model/orders.go
Normal file
31
backend/database/schemas/public/model/orders.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
117
backend/database/schemas/public/table/orders.go
Normal file
117
backend/database/schemas/public/table/orders.go
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ package table
|
|||||||
func UseSchema(schema string) {
|
func UseSchema(schema string) {
|
||||||
Medias = Medias.FromSchema(schema)
|
Medias = Medias.FromSchema(schema)
|
||||||
Migrations = Migrations.FromSchema(schema)
|
Migrations = Migrations.FromSchema(schema)
|
||||||
|
Orders = Orders.FromSchema(schema)
|
||||||
Posts = Posts.FromSchema(schema)
|
Posts = Posts.FromSchema(schema)
|
||||||
UserPosts = UserPosts.FromSchema(schema)
|
UserPosts = UserPosts.FromSchema(schema)
|
||||||
Users = Users.FromSchema(schema)
|
Users = Users.FromSchema(schema)
|
||||||
|
|||||||
@@ -16,7 +16,11 @@ types:
|
|||||||
status: PostStatus
|
status: PostStatus
|
||||||
assets: Json[[]MediaAsset]
|
assets: Json[[]MediaAsset]
|
||||||
tags: Json[[]string]
|
tags: Json[[]string]
|
||||||
meta: PostMeta
|
meta: Json[PostMeta]
|
||||||
|
|
||||||
users:
|
users:
|
||||||
status: UserStatus
|
status: UserStatus
|
||||||
|
|
||||||
|
orders:
|
||||||
|
status: OrderStatus
|
||||||
|
meta: Json[OrderMeta]
|
||||||
|
|||||||
@@ -51,3 +51,7 @@ DELETE {{host}}/v1/admin/posts/103 HTTP/1.1
|
|||||||
### get users
|
### get users
|
||||||
GET {{host}}/v1/admin/users HTTP/1.1
|
GET {{host}}/v1/admin/users HTTP/1.1
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
|
### get orders
|
||||||
|
GET {{host}}/v1/admin/orders HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
@@ -27,6 +27,11 @@ const navItems = ref([
|
|||||||
icon: 'pi pi-users',
|
icon: 'pi pi-users',
|
||||||
command: () => router.push('/users')
|
command: () => router.push('/users')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Orders',
|
||||||
|
icon: 'pi pi-shopping-cart',
|
||||||
|
command: () => router.push('/orders')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
icon: 'pi pi-cog',
|
icon: 'pi pi-cog',
|
||||||
|
|||||||
16
frontend/admin/src/api/orderService.js
Normal file
16
frontend/admin/src/api/orderService.js
Normal file
@@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
frontend/admin/src/api/order_list.json
Normal file
24
frontend/admin/src/api/order_list.json
Normal file
@@ -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": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
207
frontend/admin/src/pages/OrderPage.vue
Normal file
207
frontend/admin/src/pages/OrderPage.vue
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
<script setup>
|
||||||
|
import { orderService } from '@/api/orderService';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
|
import utc from 'dayjs/plugin/utc';
|
||||||
|
import Badge from 'primevue/badge';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import Column from 'primevue/column';
|
||||||
|
import ConfirmDialog from 'primevue/confirmdialog';
|
||||||
|
import DataTable from 'primevue/datatable';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import ProgressSpinner from 'primevue/progressspinner';
|
||||||
|
import Toast from 'primevue/toast';
|
||||||
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
const confirm = useConfirm();
|
||||||
|
|
||||||
|
const globalFilterValue = ref('');
|
||||||
|
const loading = ref(false);
|
||||||
|
const searchTimeout = ref(null);
|
||||||
|
const filters = ref({
|
||||||
|
global: { value: null, matchMode: 'contains' },
|
||||||
|
status: { value: null, matchMode: 'equals' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const orders = ref({
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
limit: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
const first = ref(0);
|
||||||
|
const rows = ref(10);
|
||||||
|
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
|
const orderStatusMap = {
|
||||||
|
0: { label: '待支付', severity: 'warning' },
|
||||||
|
1: { label: '已支付', severity: 'success' },
|
||||||
|
2: { label: '已退款', severity: 'info' },
|
||||||
|
3: { label: '已取消', severity: 'danger' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPrice = (price) => {
|
||||||
|
return (price / 100).toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDiscountAmount = (price, discount) => {
|
||||||
|
return (price * discount / 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFinalPrice = (price, discount) => {
|
||||||
|
return price - getDiscountAmount(price, discount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOrders = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const currentPage = (first.value / rows.value) + 1;
|
||||||
|
const response = await orderService.getOrders({
|
||||||
|
page: currentPage,
|
||||||
|
limit: rows.value,
|
||||||
|
keyword: globalFilterValue.value
|
||||||
|
});
|
||||||
|
orders.value = response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch orders:', error);
|
||||||
|
toast.add({ severity: 'error', summary: '错误', detail: '加载订单数据失败', life: 3000 });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPage = (event) => {
|
||||||
|
first.value = event.first;
|
||||||
|
rows.value = event.rows;
|
||||||
|
fetchOrders();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearch = (event) => {
|
||||||
|
if (searchTimeout.value) {
|
||||||
|
clearTimeout(searchTimeout.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTimeout.value = setTimeout(() => {
|
||||||
|
first.value = 0;
|
||||||
|
fetchOrders();
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
return dayjs.tz(date, 'Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (order) => {
|
||||||
|
confirm.require({
|
||||||
|
message: `确定要删除订单 "${order.id}" 吗?`,
|
||||||
|
header: '确认删除',
|
||||||
|
icon: 'pi pi-exclamation-triangle',
|
||||||
|
acceptClass: 'p-button-danger',
|
||||||
|
accept: async () => {
|
||||||
|
try {
|
||||||
|
await orderService.deleteOrder(order.id);
|
||||||
|
toast.add({ severity: 'success', summary: '成功', detail: '订单已删除', life: 3000 });
|
||||||
|
fetchOrders();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete order:', error);
|
||||||
|
toast.add({ severity: 'error', summary: '错误', detail: '删除订单失败', life: 3000 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchOrders();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Toast />
|
||||||
|
<ConfirmDialog />
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-800">订单列表</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="pb-10 flex">
|
||||||
|
<InputText v-model="globalFilterValue" placeholder="搜索订单..." class="flex-1" @input="onSearch" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable v-model:filters="filters" :value="orders.items" :paginator="true" :rows="rows"
|
||||||
|
:totalRecords="orders.total" :loading="loading" :lazy="true" :first="first" @page="onPage"
|
||||||
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||||
|
:rowsPerPageOptions="[10, 25, 50]"
|
||||||
|
currentPageReportTemplate="显示第 {first} 到 {last} 条,共 {totalRecords} 条结果" dataKey="id"
|
||||||
|
:globalFilterFields="['order_no', 'user_id']" stripedRows removableSort class="p-datatable-sm"
|
||||||
|
responsiveLayout="scroll">
|
||||||
|
|
||||||
|
<template #empty>
|
||||||
|
<div class="text-center p-4">未找到订单。</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #loading>
|
||||||
|
<div class="flex flex-col items-center justify-center p-4">
|
||||||
|
<ProgressSpinner style="width:50px;height:50px" />
|
||||||
|
<span class="mt-2">加载订单数据...</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Column field="id" header="ID" sortable></Column>
|
||||||
|
<Column field="order_no" header="订单号" sortable>
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-gray-700">系统订单号: {{ data.order_no }}</span>
|
||||||
|
<span class="text-gray-500">商户订单号: {{ data.transaction_id || '-' }}</span>
|
||||||
|
<span class="text-orange-500">退款订单号: {{ data.refund_transaction_id || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="status" header="状态" sortable>
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Badge :value="orderStatusMap[data.status]?.label"
|
||||||
|
:severity="orderStatusMap[data.status]?.severity" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="user_id" header="用户ID" sortable></Column>
|
||||||
|
<Column field="post_id" header="文章ID" sortable></Column>
|
||||||
|
<Column field="price" header="价格信息" sortable>
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-gray-500">原价: ¥{{ formatPrice(data.price) }}</span>
|
||||||
|
<span class="text-orange-500">优惠: -¥{{ formatPrice(getDiscountAmount(data.price,
|
||||||
|
data.discount)) }}</span>
|
||||||
|
<span class="font-bold">实付: ¥{{ formatPrice(getFinalPrice(data.price, data.discount))
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="updated_at" header="时间信息" sortable>
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-gray-500">更新: {{ formatDate(data.updated_at) }}</span>
|
||||||
|
<span class="text-gray-400">创建: {{ formatDate(data.created_at) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column header="操作" :exportable="false" style="min-width:8rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex justify-center space-x-2">
|
||||||
|
<Button icon="pi pi-trash" rounded text severity="danger" @click="handleDelete(data)"
|
||||||
|
aria-label="删除" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -39,6 +39,11 @@ const routes = [
|
|||||||
path: '/users',
|
path: '/users',
|
||||||
name: 'Users',
|
name: 'Users',
|
||||||
component: () => import('./pages/UserPage.vue'),
|
component: () => import('./pages/UserPage.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/orders',
|
||||||
|
name: 'Orders',
|
||||||
|
component: () => import('./pages/OrderPage.vue'),
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user