feat: add orders

This commit is contained in:
yanghao05
2025-04-10 21:42:13 +08:00
parent 5a63eee1ce
commit 8baab46132
20 changed files with 979 additions and 1 deletions

View 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)
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View 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
}

View 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)
})
}

View File

@@ -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 {

View 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
}

View File

@@ -0,0 +1,7 @@
package fields
// swagger:enum OrderStatus
// ENUM( pending, paid, refunding, refunded, cancelled, completed)
type OrderStatus int16
type OrderMeta struct{}

View 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

View 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"`
}

View 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,
}
}

View File

@@ -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)

View File

@@ -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]

View File

@@ -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

View File

@@ -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',

View 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}`);
}
}

View 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": {}
}
]
}

View 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>

View File

@@ -39,6 +39,11 @@ const routes = [
path: '/users',
name: 'Users',
component: () => import('./pages/UserPage.vue'),
},
{
path: '/orders',
name: 'Orders',
component: () => import('./pages/OrderPage.vue'),
}
];