feat(admin-payment): 支付订单 - 补充用户身份与详情
This commit is contained in:
@@ -101,10 +101,22 @@ type PaymentOrderView struct {
|
||||
}
|
||||
|
||||
type AdminPaymentOrderPage struct {
|
||||
Orders []PaymentOrderView `json:"orders"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"pageSize"`
|
||||
Orders []AdminPaymentOrderView `json:"orders"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"pageSize"`
|
||||
}
|
||||
|
||||
type AdminPaymentOrderUser struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type AdminPaymentOrderView struct {
|
||||
PaymentOrderView
|
||||
User *AdminPaymentOrderUser `json:"user"`
|
||||
}
|
||||
|
||||
func (s *Service) PaymentNotificationResponse(providerID string, success bool) (int, string, string) {
|
||||
@@ -876,9 +888,21 @@ func (s *Service) AdminPaymentOrderPage(actor *model.User, status, keyword strin
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
views := make([]PaymentOrderView, 0, len(orders))
|
||||
userIDs := make([]string, 0, len(orders))
|
||||
for _, order := range orders {
|
||||
views = append(views, paymentOrderView(order))
|
||||
userIDs = append(userIDs, order.UserID)
|
||||
}
|
||||
users, err := s.repo.UsersByIDs(userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
views := make([]AdminPaymentOrderView, 0, len(orders))
|
||||
for _, order := range orders {
|
||||
view := AdminPaymentOrderView{PaymentOrderView: paymentOrderView(order)}
|
||||
if user, ok := users[order.UserID]; ok {
|
||||
view.User = &AdminPaymentOrderUser{ID: user.ID, Username: user.Username, DisplayName: user.DisplayName, Email: user.Email}
|
||||
}
|
||||
views = append(views, view)
|
||||
}
|
||||
return &AdminPaymentOrderPage{Orders: views, Total: total, Page: page, Limit: limit}, nil
|
||||
}
|
||||
|
||||
39
backend/internal/app/payment_order_identity_test.go
Normal file
39
backend/internal/app/payment_order_identity_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPaymentOrderIdentityOnlyInAdminList(t *testing.T) {
|
||||
base := PaymentOrderView{ID: "order", UserID: "user", MerchantOrderNo: "merchant"}
|
||||
ordinary, err := json.Marshal(base)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(ordinary), `"user":`) || strings.Contains(string(ordinary), `"email":`) {
|
||||
t.Fatalf("ordinary response includes identity: %s", ordinary)
|
||||
}
|
||||
admin, err := json.Marshal(AdminPaymentOrderView{PaymentOrderView: base, User: &AdminPaymentOrderUser{ID: "user", Username: "alice", DisplayName: "小林", Email: "alice@example.com"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var result struct {
|
||||
ID string `json:"id"`
|
||||
User *AdminPaymentOrderUser `json:"user"`
|
||||
}
|
||||
if err := json.Unmarshal(admin, &result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.ID != "order" || result.User == nil || result.User.Email != "alice@example.com" {
|
||||
t.Fatalf("invalid admin response: %s", admin)
|
||||
}
|
||||
missing, err := json.Marshal(AdminPaymentOrderView{PaymentOrderView: base})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(missing), `"user":null`) || !strings.Contains(string(missing), `"userId":"user"`) {
|
||||
t.Fatalf("missing user loses original identity: %s", missing)
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func (r *Repository) UsersByIDs(ids []string) (map[string]model.User, error) {
|
||||
return map[string]model.User{}, nil
|
||||
}
|
||||
var users []model.User
|
||||
if err := r.db.Select("id", "username", "display_name").Where("id IN ?", ids).Find(&users).Error; err != nil {
|
||||
if err := r.db.Select("id", "username", "display_name", "email").Where("id IN ?", ids).Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]model.User, len(users))
|
||||
|
||||
@@ -391,7 +391,8 @@ func (r *Repository) AdminPaymentOrders(status, keyword string, limit, offset in
|
||||
}
|
||||
if normalized := strings.TrimSpace(keyword); normalized != "" {
|
||||
like := "%" + normalized + "%"
|
||||
query = query.Where("merchant_order_no LIKE ? OR provider_trade_no LIKE ? OR user_id LIKE ?", like, like, like)
|
||||
users := r.db.Model(&model.User{}).Select("id").Where("LOWER(username) LIKE ? OR LOWER(display_name) LIKE ? OR LOWER(email) LIKE ?", strings.ToLower(like), strings.ToLower(like), strings.ToLower(like))
|
||||
query = query.Where("merchant_order_no LIKE ? OR provider_trade_no LIKE ? OR user_id = ? OR user_id IN (?)", like, like, normalized, users)
|
||||
}
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
|
||||
@@ -363,6 +363,50 @@ func TestUnresolvedPaymentOrderCandidateCountOverlapping(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminPaymentOrdersSearchUserIdentity(t *testing.T) {
|
||||
db := openPaymentTestDB(t)
|
||||
if err := db.AutoMigrate(&model.User{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
users := []model.User{
|
||||
{ID: "user-alice", Username: "alice", DisplayName: "小林", Email: "alice@example.com"},
|
||||
{ID: "user-bob", Username: "bob", DisplayName: "小林", Email: "bob@example.com"},
|
||||
}
|
||||
if err := db.Create(&users).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
orders := []model.PaymentOrder{
|
||||
{ID: "a", UserID: "user-alice", MerchantOrderNo: "merchant-a", IdempotencyKey: "a", Status: model.PaymentOrderPending},
|
||||
{ID: "b", UserID: "user-bob", MerchantOrderNo: "merchant-b", IdempotencyKey: "b", Status: model.PaymentOrderClosed},
|
||||
{ID: "c", UserID: "missing-user", MerchantOrderNo: "merchant-c", IdempotencyKey: "c", Status: model.PaymentOrderClosed},
|
||||
}
|
||||
tradeNo := "trade-c"
|
||||
orders[2].ProviderTradeNo = &tradeNo
|
||||
if err := db.Create(&orders).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repo := New(db)
|
||||
for _, tc := range []struct {
|
||||
keyword, status string
|
||||
total int64
|
||||
}{
|
||||
{"ALICE@EXAMPLE.COM", "", 1}, {"alice", "", 1}, {"小林", "", 2},
|
||||
{"小林", "pending", 1}, {"alice", "closed", 0}, {"user-bob", "", 1},
|
||||
{"merchant-c", "", 1}, {"trade-c", "", 1}, {"missing-user", "", 1}, {"", "", 3},
|
||||
} {
|
||||
t.Run(tc.keyword+"/"+tc.status, func(t *testing.T) {
|
||||
items, total, err := repo.AdminPaymentOrders(tc.status, tc.keyword, 1, 0)
|
||||
if err != nil || total != tc.total || len(items) > 1 {
|
||||
t.Fatalf("got items=%d total=%d err=%v; want total=%d", len(items), total, err, tc.total)
|
||||
}
|
||||
})
|
||||
}
|
||||
identities, err := repo.UsersByIDs([]string{"user-alice", "missing-user"})
|
||||
if err != nil || len(identities) != 1 || identities["user-alice"].Email != "alice@example.com" {
|
||||
t.Fatalf("identities=%v err=%v", identities, err)
|
||||
}
|
||||
}
|
||||
|
||||
func openPaymentTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared&_busy_timeout=5000"), &gorm.Config{})
|
||||
|
||||
@@ -9,6 +9,14 @@ description: 信封、错误码与 OpenAPI
|
||||
|
||||
机器可读规范:运行中的 `GET /api/openapi.yaml`,源文件 `backend/internal/handler/openapi.yaml`。前端 TypeScript 仍走 `web/src/services/api` 的 `http`,不从 OpenAPI 生成客户端。
|
||||
|
||||
## 管理员支付订单
|
||||
|
||||
`GET /api/admin/payments/orders` 需要管理员权限,支持 `status`、`keyword`、`page`、`pageSize`。关键词匹配订单号、渠道交易号、用户名称、用户名或邮箱,也支持完整用户 ID 精确匹配。用户名和邮箱搜索忽略大小写;状态过滤同时生效,返回匹配总数和当前页订单。
|
||||
|
||||
每条管理员列表订单在原有字段之外包含 `user: { id, username, displayName, email }`,由当前页用户 ID 批量读取;用户不存在时返回 `user: null`,保留原 `userId` 与历史订单。此身份摘要仅用于管理员订单列表,不添加到普通用户支付接口或支付状态同步响应。
|
||||
|
||||
后台支付订单列表展示用户身份,点击名称打开用户详情;订单详情提供完整订单号、渠道交易号、用户 ID 和各阶段时间。同步支付状态与关闭订单位于更多菜单,关闭订单仍需确认。
|
||||
|
||||
## 信封
|
||||
|
||||
成功:
|
||||
|
||||
@@ -1636,3 +1636,11 @@ v2(用户反馈首版难看后重做):菜单改用 antd Dropdown(placeme
|
||||
- [ ] 套餐未开通等上游 403 场景,上传失败提示包含方舟原始错误码(如 SubscriptionRequired),不再是「模型服务鉴权失败」。
|
||||
- [ ] 素材库编辑图片素材:可填写 asset- 开头的方舟素材 ID、勾选人像认证,保存后刷新页面两字段仍在;卡片封面出现「人像认证」徽章,详情抽屉显示方舟素材 ID 标签;非法 ID(不以 asset- 开头)被表单拦截。
|
||||
- [ ] 画布图片节点绑定已录入方舟素材 ID 的素材后连接视频节点,用方舟渠道生成:任务参考图为 asset:// 引用、跳过「同步方舟可信素材」;非方舟渠道生成不受影响,仍走原参考图路径。
|
||||
## 支付订单用户识别(待验收)
|
||||
|
||||
已通过支付页与 API 的 ESLint、15 项后台 UI 回归检查,以及后端身份响应隔离测试和相关包编译。全量前端类型检查仍在未修改的动画、Tooltip 和画布模块报错;数据库搜索专项测试因本机 `CGO_ENABLED=0` 无法执行。尚未进行浏览器视觉验收。
|
||||
|
||||
- 核对名称、用户名和邮箱与用户管理一致;同名用户可通过用户名和邮箱区分。
|
||||
- 验收用户身份、订单号、渠道交易号和完整用户 ID 查询,与状态过滤、分页组合使用。
|
||||
- 验收用户详情、订单详情、订单号复制和用户不存在的历史订单;关闭侧栏保留列表状态。
|
||||
- 检查同步状态、关单确认、明暗主题、长文本和窄屏固定操作列。
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AlipayCircleFilled, WechatFilled } from "@ant-design/icons";
|
||||
import { Callout } from "@/components/ui/product/callout";
|
||||
import { App, Button, DatePicker, Drawer, Form, Input, InputNumber, Select, Tabs } from "antd";
|
||||
import { App, Button, DatePicker, Descriptions, Drawer, Form, Input, InputNumber, Select, Tabs, Typography } from "antd";
|
||||
import { AppDrawer } from "@/components/ui/product/app-drawer";
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import dayjs, { type Dayjs } from "dayjs";
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
updateAdminPaymentProvider,
|
||||
updateAdminTopupProduct,
|
||||
type AdminPaymentProvider,
|
||||
type AdminPaymentOrder,
|
||||
type PaymentOrder,
|
||||
type PaymentReconciliationItem,
|
||||
type PaymentReconciliationRun,
|
||||
@@ -29,7 +31,8 @@ import {
|
||||
} from "@/services/api/payments";
|
||||
|
||||
import { AdminPageFrame } from "../components/admin-shell";
|
||||
import { AdminDataTable, AdminStatusBadge, AdminTableEmpty, configuredSecretText } from "../components/admin-ui";
|
||||
import { AdminDataTable, AdminRowActions, AdminStatusBadge, AdminTableEmpty, configuredSecretText } from "../components/admin-ui";
|
||||
import { AdminUserDetailDrawer } from "../components/admin-user-detail-drawer";
|
||||
import "./payments-page.css";
|
||||
|
||||
type ProviderFormValues = {
|
||||
@@ -81,7 +84,9 @@ export default function AdminPaymentsPage() {
|
||||
const [productSaving, setProductSaving] = useState(false);
|
||||
const [productForm] = Form.useForm<ProductFormValues>();
|
||||
|
||||
const [orders, setOrders] = useState<PaymentOrder[]>([]);
|
||||
const [orders, setOrders] = useState<AdminPaymentOrder[]>([]);
|
||||
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
|
||||
const [selectedOrder, setSelectedOrder] = useState<AdminPaymentOrder | null>(null);
|
||||
const [orderTotal, setOrderTotal] = useState(0);
|
||||
const [orderPage, setOrderPage] = useState(1);
|
||||
const [orderPageSize, setOrderPageSize] = useState(30);
|
||||
@@ -378,16 +383,35 @@ export default function AdminPaymentsPage() {
|
||||
},
|
||||
];
|
||||
|
||||
const orderColumns: ColumnsType<PaymentOrder> = [
|
||||
const orderColumns: ColumnsType<AdminPaymentOrder> = [
|
||||
{
|
||||
title: "订单",
|
||||
title: "用户",
|
||||
key: "user",
|
||||
width: 230,
|
||||
render: (_, order) => (
|
||||
<div className="min-w-0">
|
||||
{order.user ? <>
|
||||
<div className="flex min-w-0 items-baseline gap-2">
|
||||
<button type="button" className="admin-table-primary-link truncate font-medium" title={order.user.displayName || order.user.username} onClick={() => setSelectedUserId(order.user!.id)}>{order.user.displayName || order.user.username}</button>
|
||||
<span className="truncate text-xs text-foreground/45" title={`@${order.user.username}`}>@{order.user.username}</span>
|
||||
</div>
|
||||
<div className="mt-1 truncate text-xs text-foreground/60" title={order.user.email}>{order.user.email || "未填写邮箱"}</div>
|
||||
</> : <>
|
||||
<div className="text-foreground/60">用户不存在</div>
|
||||
<Typography.Text className="text-xs" copyable={order.userId ? { text: order.userId } : false}>{order.userId ? `${order.userId.slice(0, 8)}…${order.userId.slice(-6)}` : "--"}</Typography.Text>
|
||||
</>}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "订单 / 商品",
|
||||
key: "order",
|
||||
width: 280,
|
||||
width: 210,
|
||||
render: (_, order) => (
|
||||
<div>
|
||||
<div className="font-mono text-xs">{order.merchantOrderNo}</div>
|
||||
<div className="mt-1 truncate text-xs text-foreground/45">
|
||||
用户 {order.userId || "--"} · {order.productName}
|
||||
<Typography.Text className="font-mono text-xs" title={order.merchantOrderNo} copyable={{ text: order.merchantOrderNo }}>{order.merchantOrderNo.length > 20 ? `${order.merchantOrderNo.slice(0, 10)}…${order.merchantOrderNo.slice(-6)}` : order.merchantOrderNo}</Typography.Text>
|
||||
<div className="mt-1 truncate text-xs text-foreground/45" title={order.productName}>
|
||||
{order.productName}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
@@ -395,7 +419,7 @@ export default function AdminPaymentsPage() {
|
||||
{
|
||||
title: "渠道",
|
||||
dataIndex: "providerId",
|
||||
width: 160,
|
||||
width: 150,
|
||||
render: (value) => (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<PaymentBrandIcon providerId={value} compact />
|
||||
@@ -406,7 +430,7 @@ export default function AdminPaymentsPage() {
|
||||
{
|
||||
title: "金额 / 积分",
|
||||
key: "amount",
|
||||
width: 150,
|
||||
width: 130,
|
||||
align: "right",
|
||||
render: (_, order) => (
|
||||
<div>
|
||||
@@ -416,29 +440,22 @@ export default function AdminPaymentsPage() {
|
||||
),
|
||||
},
|
||||
{ title: "状态", dataIndex: "status", width: 110, align: "center", render: (value) => <AdminStatusBadge {...(paymentOrderStatus[value] || { label: value, tone: "neutral" as const })} /> },
|
||||
{ title: "创建时间", dataIndex: "createdAt", width: 170, render: (value) => formatDateTime(value) },
|
||||
{ title: "创建时间", dataIndex: "createdAt", width: 130, render: (value) => <span title={formatDateTime(value)}>{dayjs(value).format("MM-DD HH:mm")}</span> },
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
width: 150,
|
||||
fixed: "right",
|
||||
align: "center",
|
||||
render: (_, order) => (
|
||||
<div className="flex justify-center gap-1">
|
||||
<Button type="text" size="small" loading={orderActionId === order.id} disabled={order.status === "credited" || order.status === "closed"} onClick={() => void queryOrder(order)}>
|
||||
查单
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<XCircle className="size-3.5" />}
|
||||
loading={orderActionId === order.id}
|
||||
disabled={!["created", "pending", "create_failed", "closing"].includes(order.status)}
|
||||
onClick={() => closeOrder(order)}
|
||||
>
|
||||
关单
|
||||
</Button>
|
||||
</div>
|
||||
<AdminRowActions
|
||||
primary={{ label: "详情", icon: <Eye className="size-3.5" />, onClick: () => setSelectedOrder(order) }}
|
||||
visibleActionCount={0}
|
||||
actions={[
|
||||
{ key: "sync", label: "同步支付状态", icon: <RefreshCw className="size-3.5" />, disabled: Boolean(orderActionId) || ["credited", "closed"].includes(order.status), onClick: () => queryOrder(order) },
|
||||
{ key: "close", label: "关闭订单", icon: <XCircle className="size-3.5" />, danger: true, disabled: Boolean(orderActionId) || !["created", "pending", "create_failed", "closing"].includes(order.status), onClick: () => closeOrder(order) },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -538,7 +555,8 @@ export default function AdminPaymentsPage() {
|
||||
allowClear
|
||||
prefix={<Search className="size-4 text-foreground/40" />}
|
||||
value={orderKeyword}
|
||||
placeholder="订单号、渠道交易号或用户 ID"
|
||||
placeholder="搜索名称、用户名、邮箱、订单号"
|
||||
title="支持名称、用户名、邮箱、订单号、渠道交易号和完整用户 ID"
|
||||
onChange={(event) => setOrderKeyword(event.target.value)}
|
||||
onPressEnter={() => void loadOrders(1)}
|
||||
/>
|
||||
@@ -552,7 +570,7 @@ export default function AdminPaymentsPage() {
|
||||
/>
|
||||
}
|
||||
trailing={<Button onClick={() => void loadOrders(1)}>查询</Button>}
|
||||
table={{ rowKey: "id", loading: ordersLoading, columns: orderColumns, dataSource: orders, pagination: false, scroll: { x: 1180 } }}
|
||||
table={{ rowKey: "id", loading: ordersLoading, columns: orderColumns, dataSource: orders, pagination: false, tableLayout: "fixed", scroll: { x: 1110 } }}
|
||||
empty={<AdminTableEmpty filtered={Boolean(orderKeyword || orderStatusFilter !== "all")} title="没有支付订单" />}
|
||||
footer={<PaginationBar alwaysShow current={orderPage} pageSize={orderPageSize} total={orderTotal} onChange={(page, size) => void loadOrders(size !== orderPageSize ? 1 : page, size)} />}
|
||||
/>
|
||||
@@ -600,6 +618,22 @@ export default function AdminPaymentsPage() {
|
||||
]}
|
||||
/>
|
||||
|
||||
<AppDrawer title="支付订单详情" size="min(680px, 100vw)" open={Boolean(selectedOrder)} onClose={() => setSelectedOrder(null)}>
|
||||
{selectedOrder && <Descriptions column={1} bordered size="small" items={[
|
||||
{ key: "user", label: "用户", children: selectedOrder.user ? <button type="button" className="admin-table-primary-link" onClick={() => { setSelectedUserId(selectedOrder.user!.id); setSelectedOrder(null); }}>{selectedOrder.user.displayName || selectedOrder.user.username} · @{selectedOrder.user.username}</button> : "用户不存在" },
|
||||
{ key: "email", label: "邮箱", children: selectedOrder.user?.email || "未填写邮箱" },
|
||||
{ key: "userId", label: "用户 ID", children: <Typography.Text copyable className="break-all">{selectedOrder.userId || "--"}</Typography.Text> },
|
||||
{ key: "order", label: "订单号", children: <Typography.Text copyable className="break-all">{selectedOrder.merchantOrderNo}</Typography.Text> },
|
||||
{ key: "trade", label: "渠道交易号", children: selectedOrder.providerTradeNo ? <Typography.Text copyable className="break-all">{selectedOrder.providerTradeNo}</Typography.Text> : "--" },
|
||||
{ key: "product", label: "商品", children: selectedOrder.productName },
|
||||
{ key: "channel", label: "支付渠道", children: providerNames[selectedOrder.providerId] || selectedOrder.providerId },
|
||||
{ key: "amount", label: "金额 / 积分", children: `¥ ${(selectedOrder.amountFen / 100).toFixed(2)} / ${formatCredits(selectedOrder.creditsMicrocredits)} 积分` },
|
||||
{ key: "status", label: "状态", children: paymentOrderStatus[selectedOrder.status]?.label || selectedOrder.status },
|
||||
...([{ key: "createdAt", label: "创建时间" }, { key: "expiresAt", label: "过期时间" }, { key: "providerPaidAt", label: "支付时间" }, { key: "creditedAt", label: "入账时间" }, { key: "closedAt", label: "关闭时间" }] as const).map(({ key, label }) => ({ key, label, children: selectedOrder[key] ? formatDateTime(selectedOrder[key]!) : "--" })),
|
||||
]} />}
|
||||
</AppDrawer>
|
||||
<AdminUserDetailDrawer userId={selectedUserId} onClose={() => setSelectedUserId(null)} />
|
||||
|
||||
<Drawer
|
||||
title={providerDrawer ? `配置 ${providerDrawer.name}` : "配置支付渠道"}
|
||||
width={620}
|
||||
|
||||
@@ -125,8 +125,12 @@ export function updateAdminTopupProduct(id: string, input: TopupProductInput) {
|
||||
return http.put<{ product: TopupProduct }>(`/admin/payments/products/${encodeURIComponent(id)}`, input);
|
||||
}
|
||||
|
||||
export type AdminPaymentOrder = PaymentOrder & {
|
||||
user: { id: string; username: string; displayName: string; email: string } | null;
|
||||
};
|
||||
|
||||
export function listAdminPaymentOrders(params: { status?: string; keyword?: string; page?: number; pageSize?: number } = {}) {
|
||||
return http.get<{ orders: PaymentOrder[]; total: number; page: number; pageSize: number }>("/admin/payments/orders", { params });
|
||||
return http.get<{ orders: AdminPaymentOrder[]; total: number; page: number; pageSize: number }>("/admin/payments/orders", { params });
|
||||
}
|
||||
|
||||
export function queryAdminPaymentOrder(id: string) {
|
||||
|
||||
Reference in New Issue
Block a user