fix(admin): 超管看不到下游订单——新增平台侧跨商户订单视图

问题:管理员首页 Dashboard 显示平台总量(37 单,跨全部商户),点进「发货订单」
却只有 29 单,两个数字互相矛盾且界面上没有任何解释。

根因是「平台管理员」在两套接口里语义不同:
- /api/dashboard:isPlatformAdmin 只看 JWT 角色 → 平台视角,跨商户聚合
- /api/merchant/*:Tenant 中间件强制落到单个 merchant_id → 商户成员视角。
  管理员唯一的成员关系是自营商户,于是被静默框定在商户 1
且 34 条 /platform/* 路由中没有任何订单入口,前端也没有商户切换器或上下文提示。

修复(不改动商户侧隔离语义):
- 新增 GET /api/platform/orders,support 按商户/状态/来源/关键字筛选,缺省为全部商户
- Service 抽出共用的 orderFilters 集中校验,ListOrders 与 ListPlatformOrders 并行;
  ListOrders 仍强制单商户,避免新增平台路径时顺手放宽商户侧可见范围
- FulfillmentOrder 增加 Merchant 关联字段,仅平台侧 Preload,商户侧响应结构不变
- 前端新增「下游订单(全部商户)」页与菜单项,「发货订单」更名为「发货订单(当前商户)」
- README 补充两个订单入口的说明,避免后来者重踩

验证:单测覆盖跨商户可见性、按商户筛选、筛选校验、以及商户侧隔离不回归;
实测新接口返回 37 单(自营 29 + 测试商户 8)且带商户信息,商户侧仍为 29 单且无 merchant 字段。
This commit is contained in:
yml2213
2026-10-06 20:28:41 +08:00
parent 97948e2ca6
commit 8c12d1d448
11 changed files with 522 additions and 14 deletions
+16
View File
@@ -191,6 +191,22 @@ POST /api/open/v1/orders/ship-notify
管理员登录后可访问商品管理、订单管理、用户管理、发货日志等功能。后台默认地址 `/`。
### 订单管理有两个入口,不要混淆
平台管理员同时拥有「平台视角」与「商户视角」两套订单入口,它们的数据范围**不同**:
| 入口 | 接口 | 数据范围 | 是否受商户上下文影响 |
|------|------|----------|----------------------|
| 下游订单(全部商户) | `GET /api/platform/orders` | 全部下游商户 | 否 |
| 发货订单(当前商户) | `GET /api/merchant/orders` | 仅当前商户 | 是,取决于 `X-Merchant-ID` |
首页 Dashboard 对管理员是**平台口径**(跨商户聚合)。如果只看「发货订单」,需要先知道
自己当前处在哪个商户上下文里,否则会出现「首页 37 单、订单页 29 单」这种看起来像 bug 的差异——
实际上订单页只显示了自营商户这一家。
商户上下文的来源是前端 `localStorage.merchant_id`,只会在「平台商户」页点「成员」时被写入。
需要看某个下游商户的订单时,用「下游订单」页的商户筛选即可,不必切换上下文。
## 目录结构
```
@@ -1,6 +1,7 @@
package handler
import (
"strconv"
"strings"
"affiliate_dash/internal/middleware"
@@ -25,6 +26,37 @@ func (h *MerchantHandler) ListOrders(c *gin.Context) {
response.Page(c, list, total, page, size)
}
// ListPlatformOrders GET /api/platform/orders
//
// 平台管理员跨商户查看下游订单。merchant_id 查询参数可选:不传表示全部商户。
// 与商户侧 /api/merchant/orders 是两条独立路径——商户侧仍强制单商户隔离,
// 这里不依赖 X-Merchant-ID,因此管理员遗留的商户上下文不会影响本接口。
func (h *MerchantHandler) ListPlatformOrders(c *gin.Context) {
page, size := pageParams(c)
var merchantID *uint
if raw := strings.TrimSpace(c.Query("merchant_id")); raw != "" {
id, err := strconv.ParseUint(raw, 10, 64)
if err != nil || id == 0 {
response.BadRequest(c, "商户 ID 无效")
return
}
value := uint(id)
merchantID = &value
}
list, total, err := h.fulfillmentSvc.ListPlatformOrders(merchantID, page, size, c.Query("order_status"), c.Query("order_source"), c.Query("keyword"))
if err != nil {
if err.Error() == "无效的订单来源" || err.Error() == "无效的订单状态" {
response.BadRequest(c, err.Error())
return
}
response.ServerError(c, err.Error())
return
}
response.Page(c, list, total, page, size)
}
type merchantManualOrderReq struct {
ClientOrderNo string `json:"client_order_no" binding:"required,max=96"`
SKU string `json:"sku" binding:"required,max=96"`
+3
View File
@@ -246,6 +246,9 @@ type FulfillmentOrder struct {
DeliveryLinkGeneratedAt *time.Time `json:"delivery_link_generated_at"`
MerchantProduct *MerchantProduct `gorm:"foreignKey:MerchantProductID" json:"merchant_product,omitempty"`
// Merchant 仅在平台侧跨商户查询时 Preload;商户侧响应里为 nil,被 omitempty 省略,
// 因此该字段不改变既有接口结构。
Merchant *Merchant `gorm:"foreignKey:MerchantID" json:"merchant,omitempty"`
}
// CallbackSubscription 为商户提供独立、可禁用的事件订阅。
+3
View File
@@ -284,6 +284,9 @@ func Setup(h *Handlers) *gin.Engine {
admin.PATCH("/platform/admins/:id", h.User.UpdatePlatformAdmin)
admin.DELETE("/platform/admins/:id", h.User.DeletePlatformAdmin)
admin.GET("/platform/merchants", h.Merchant.ListPlatformMerchants)
// 跨商户订单视图:商户后台的 /api/merchant/orders 强制单商户隔离,
// 超管需要一条平台视角入口来查看全部下游订单。
admin.GET("/platform/orders", h.Merchant.ListPlatformOrders)
admin.POST("/platform/merchants", h.Merchant.CreateMerchant)
admin.PATCH("/platform/merchants/:id", h.Merchant.UpdateMerchantSettings)
admin.POST("/platform/merchants/:id/members", h.Merchant.AddPlatformMerchantMember)
@@ -0,0 +1,135 @@
package service
import (
"testing"
"affiliate_dash/internal/model"
)
// seedPlatformOrders 为两个商户各造若干订单,用于平台侧跨商户查询测试。
func seedPlatformOrders(t *testing.T) (*FulfillmentService, uint, uint) {
t.Helper()
db := newServiceTestDB(t)
svc := NewFulfillmentService(db, nil)
merchantA, productA := seedFulfillmentMerchant(t, db, "pf-merchant-a", 0, -1, 100)
merchantB, productB := seedFulfillmentMerchant(t, db, "pf-merchant-b", 0, -1, 100)
orders := []model.FulfillmentOrder{
{MerchantID: merchantA, OrderNo: "A-1", ClientOrderNo: "A-C1", MerchantProductID: productA.ID, ProductSKU: productA.SKU, ProductName: "商品A", Amount: 100, OrderStatus: model.OrderStatusPaid, OrderSource: model.OrderSourceAPI},
{MerchantID: merchantA, OrderNo: "A-2", ClientOrderNo: "A-C2", MerchantProductID: productA.ID, ProductSKU: productA.SKU, ProductName: "商品A", Amount: 100, OrderStatus: model.OrderStatusDelivered, OrderSource: model.OrderSourceManual},
{MerchantID: merchantB, OrderNo: "B-1", ClientOrderNo: "B-C1", MerchantProductID: productB.ID, ProductSKU: productB.SKU, ProductName: "商品B", Amount: 200, OrderStatus: model.OrderStatusPaid, OrderSource: model.OrderSourceAPI},
}
for i := range orders {
if err := db.Create(&orders[i]).Error; err != nil {
t.Fatalf("create order: %v", err)
}
}
return svc, merchantA, merchantB
}
// TestListPlatformOrdersSpansMerchants 是本次修复的核心断言:
// 平台视角必须能同时看到全部商户的订单,并带出订单所属商户。
func TestListPlatformOrdersSpansMerchants(t *testing.T) {
svc, merchantA, merchantB := seedPlatformOrders(t)
list, total, err := svc.ListPlatformOrders(nil, 1, 20, "", "", "")
if err != nil {
t.Fatalf("平台侧跨商户查询失败: %v", err)
}
if total != 3 || len(list) != 3 {
t.Fatalf("期望 3 单(A 2 单 + B 1 单),实际 total=%d len=%d", total, len(list))
}
seen := map[uint]int{}
for _, order := range list {
seen[order.MerchantID]++
// 跨商户列表必须能说明每单属于哪个商户,否则管理员无法分辨。
if order.Merchant == nil {
t.Fatalf("订单 %s 未预加载商户信息", order.OrderNo)
}
}
if seen[merchantA] != 2 || seen[merchantB] != 1 {
t.Fatalf("商户分布不符:%v", seen)
}
}
// TestListPlatformOrdersFiltersByMerchant 指定 merchant_id 时只返回该商户。
func TestListPlatformOrdersFiltersByMerchant(t *testing.T) {
svc, merchantA, _ := seedPlatformOrders(t)
list, total, err := svc.ListPlatformOrders(&merchantA, 1, 20, "", "", "")
if err != nil {
t.Fatalf("按商户筛选失败: %v", err)
}
if total != 2 || len(list) != 2 {
t.Fatalf("期望商户 A 的 2 单,实际 total=%d len=%d", total, len(list))
}
for _, order := range list {
if order.MerchantID != merchantA {
t.Fatalf("出现了其他商户的订单:merchant_id=%d", order.MerchantID)
}
}
}
// TestListPlatformOrdersAppliesFilters 平台侧同样要支持状态/来源/关键字筛选与非法值校验。
func TestListPlatformOrdersAppliesFilters(t *testing.T) {
svc, _, _ := seedPlatformOrders(t)
cases := []struct {
name string
status string
source string
keyword string
wantTotal int64
wantErrText string
}{
{name: "按状态筛选", status: model.OrderStatusPaid, wantTotal: 2},
{name: "按来源筛选", source: model.OrderSourceManual, wantTotal: 1},
{name: "按订单号关键字", keyword: "B-", wantTotal: 1},
{name: "按商户单号关键字", keyword: "A-C", wantTotal: 2},
{name: "非法状态", status: "not-a-status", wantErrText: "无效的订单状态"},
{name: "非法来源", source: "not-a-source", wantErrText: "无效的订单来源"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
list, total, err := svc.ListPlatformOrders(nil, 1, 20, tc.status, tc.source, tc.keyword)
if tc.wantErrText != "" {
if err == nil || err.Error() != tc.wantErrText {
t.Fatalf("期望错误 %q,实际 %v", tc.wantErrText, err)
}
return
}
if err != nil {
t.Fatalf("查询失败: %v", err)
}
if total != tc.wantTotal || int64(len(list)) != tc.wantTotal {
t.Fatalf("期望 %d 单,实际 total=%d len=%d", tc.wantTotal, total, len(list))
}
})
}
}
// TestListOrdersStillIsolatesSingleMerchant 卡住商户侧隔离语义:
// 这次修复新增了平台侧路径,但绝不能顺手放宽商户侧的可见范围。
// 同时商户侧响应不应带上 Merchant(保持既有接口结构不变)。
func TestListOrdersStillIsolatesSingleMerchant(t *testing.T) {
svc, merchantA, _ := seedPlatformOrders(t)
list, total, err := svc.ListOrders(merchantA, 1, 20, "", "", "")
if err != nil {
t.Fatalf("商户侧查询失败: %v", err)
}
if total != 2 || len(list) != 2 {
t.Fatalf("商户侧必须只看到自己的 2 单,实际 total=%d len=%d", total, len(list))
}
for _, order := range list {
if order.MerchantID != merchantA {
t.Fatalf("商户侧串到了其他商户的订单:merchant_id=%d", order.MerchantID)
}
if order.Merchant != nil {
t.Fatal("商户侧不应预加载 Merchant,否则会改变既有响应结构")
}
}
}
+61 -13
View File
@@ -24,38 +24,86 @@ func (s *FulfillmentService) GetOrder(merchantID uint, orderNo string) (*model.F
return &order, nil
}
func (s *FulfillmentService) ListOrders(merchantID uint, page, size int, orderStatus, orderSource, keyword string) ([]model.FulfillmentOrder, int64, error) {
page, size = normalizePage(page, size)
tx := s.db.Model(&model.FulfillmentOrder{}).Where("merchant_id = ?", merchantID)
if orderStatus != "" {
switch orderStatus {
// orderFilters 是商户侧与平台侧共用的订单筛选条件。
// 集中在一处校验,避免以后新增筛选项时只改了一边。
type orderFilters struct {
// merchantID 为 nil 表示不限商户(仅平台侧使用);指向 0 时仍会按 merchant_id=0 过滤,
// 以保持 ListOrders 的原有语义不变。
merchantID *uint
orderStatus string
orderSource string
keyword string
}
func (f orderFilters) apply(tx *gorm.DB) (*gorm.DB, error) {
if f.merchantID != nil {
tx = tx.Where("merchant_id = ?", *f.merchantID)
}
if f.orderStatus != "" {
switch f.orderStatus {
case model.OrderStatusPaid, model.OrderStatusDelivering, model.OrderStatusDelivered, model.OrderStatusShipFailed, model.OrderStatusCancelled:
default:
return nil, 0, errors.New("无效的订单状态")
return nil, errors.New("无效的订单状态")
}
tx = tx.Where("order_status = ?", orderStatus)
tx = tx.Where("order_status = ?", f.orderStatus)
}
if orderSource != "" {
switch orderSource {
if f.orderSource != "" {
switch f.orderSource {
case model.OrderSourceAPI, model.OrderSourceManual, model.OrderSourceTest:
tx = tx.Where("order_source = ?", orderSource)
tx = tx.Where("order_source = ?", f.orderSource)
default:
return nil, 0, errors.New("无效的订单来源")
return nil, errors.New("无效的订单来源")
}
}
if keyword = strings.TrimSpace(keyword); keyword != "" {
if keyword := strings.TrimSpace(f.keyword); keyword != "" {
like := "%" + keyword + "%"
tx = tx.Where("order_no LIKE ? OR client_order_no LIKE ?", like, like)
}
return tx, nil
}
func (s *FulfillmentService) listOrders(f orderFilters, page, size int, withMerchant bool) ([]model.FulfillmentOrder, int64, error) {
page, size = normalizePage(page, size)
tx, err := f.apply(s.db.Model(&model.FulfillmentOrder{}))
if err != nil {
return nil, 0, err
}
var total int64
if err := tx.Count(&total).Error; err != nil {
return nil, 0, err
}
query := tx.Preload("MerchantProduct.Product")
if withMerchant {
// 平台侧跨商户列表需要展示订单属于哪个商户。
query = query.Preload("Merchant")
}
var orders []model.FulfillmentOrder
err := tx.Preload("MerchantProduct.Product").Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&orders).Error
err = query.Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&orders).Error
return orders, total, err
}
// ListOrders 商户后台订单列表,强制按单个商户隔离。
func (s *FulfillmentService) ListOrders(merchantID uint, page, size int, orderStatus, orderSource, keyword string) ([]model.FulfillmentOrder, int64, error) {
return s.listOrders(orderFilters{
merchantID: &merchantID,
orderStatus: orderStatus,
orderSource: orderSource,
keyword: keyword,
}, page, size, false)
}
// ListPlatformOrders 平台侧跨商户订单查询,供超管在管理端查看全部下游订单。
// merchantID 为 nil 表示不限商户。这是与 ListOrders 并行的另一条路径,
// 刻意不改动商户侧「必须落到单个商户」的隔离语义。
func (s *FulfillmentService) ListPlatformOrders(merchantID *uint, page, size int, orderStatus, orderSource, keyword string) ([]model.FulfillmentOrder, int64, error) {
return s.listOrders(orderFilters{
merchantID: merchantID,
orderStatus: orderStatus,
orderSource: orderSource,
keyword: keyword,
}, page, size, true)
}
type OpenOrderQuery struct {
OrderNo string `json:"order_no"`
Status string `json:"status"`
+9
View File
@@ -19,6 +19,7 @@ const MerchantCenter = lazy(() => import('./pages/MerchantCenter'))
const MerchantRecharge = lazy(() => import('./pages/MerchantRecharge'))
const AlertSettings = lazy(() => import('./pages/AlertSettings'))
const PlatformMerchants = lazy(() => import('./pages/PlatformMerchants'))
const PlatformOrders = lazy(() => import('./pages/PlatformOrders'))
const PlatformProducts = lazy(() => import('./pages/PlatformProducts'))
const PlatformUsers = lazy(() => import('./pages/PlatformUsers'))
const PlatformRock = lazy(() => import('./pages/PlatformRock'))
@@ -92,6 +93,14 @@ function AppRoutes() {
<Route path="merchant-members" element={<MerchantPermissionRoute permission="members:manage"><MerchantCenter fixedTab="members" title="成员" /></MerchantPermissionRoute>} />
<Route path="merchant-callbacks" element={<MerchantPermissionRoute permission="callbacks:manage"><MerchantCenter fixedTab="callbacks" title="回调" /></MerchantPermissionRoute>} />
<Route path="merchant-api-keys" element={<MerchantPermissionRoute permission="api:manage"><MerchantCenter fixedTab="api" title="API 密钥" /></MerchantPermissionRoute>} />
<Route
path="platform-orders"
element={
<AdminRoute>
<PlatformOrders />
</AdminRoute>
}
/>
<Route
path="platform-merchants"
element={
+3
View File
@@ -207,6 +207,9 @@ export const platformApi = {
request.delete(`/platform/merchant-accounts/${userId}`).then((r) => r.data.data),
merchants: (params?: Record<string, unknown>) =>
request.get('/platform/merchants', { params }).then((r) => r.data.data as PageResult<Merchant>),
// 平台侧跨商户订单视图:不传 merchant_id 即全部商户
orders: (params?: Record<string, unknown>) =>
request.get('/platform/orders', { params }).then((r) => r.data.data as PageResult<FulfillmentOrder>),
createMerchant: (data: {
code: string
name: string
+4 -1
View File
@@ -88,7 +88,10 @@ const adminSections: SidebarSection[] = [
key: 'orders',
label: '订单管理',
icon: <OrderedListOutlined />,
children: [{ key: 'fulfillment-orders', label: '发货订单', path: '/merchant-orders' }],
children: [
{ key: 'platform-orders', label: '下游订单(全部商户)', path: '/platform-orders' },
{ key: 'fulfillment-orders', label: '发货订单(当前商户)', path: '/merchant-orders' },
],
},
{
key: 'staff',
+250
View File
@@ -0,0 +1,250 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Button, Card, Col, Empty, Input, Row, Select, Space, Table, Tag, Typography, message } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { ReloadOutlined, SearchOutlined } from '@ant-design/icons'
import { platformApi } from '../api'
import { PageHeader } from '../components/PageHeader'
import type { FulfillmentOrder, Merchant, PageResult } from '../types'
import { formatDateTime } from '../utils/time'
import { money, orderStatusMap, orderStatusOptions, pageConfig } from './merchantCenterUtils'
const { Text } = Typography
const sourceOptions = [
{ value: '', label: '全部来源' },
{ value: 'api', label: '开放接口' },
{ value: 'manual', label: '人工下单' },
{ value: 'test', label: '联调测试' },
]
const sourceText: Record<string, string> = {
api: '开放接口',
manual: '人工下单',
test: '联调测试',
}
interface OrderFilters {
merchantId?: number
orderStatus?: string
orderSource?: string
keyword?: string
}
const emptyPage: PageResult<FulfillmentOrder> = { list: [], total: 0, page: 1, size: 20 }
function orderStatusTag(value: string) {
const item = orderStatusMap[value] || { color: 'default', text: value }
return <Tag color={item.color}>{item.text}</Tag>
}
/**
* 平台侧「下游订单」页。
*
* 存在的理由:商户后台的「发货订单」走 /api/merchant/orders,服务端强制按单个商户隔离,
* 而管理员的默认商户上下文是自营商户——于是首页 Dashboard 显示平台总量(跨商户),
* 订单页却只显示自营商户的量,两个数字对不上且无法解释。本页走 /api/platform/orders,
* 不依赖 X-Merchant-ID,管理员可以在这里看到全部下游商户的订单。
*/
export default function PlatformOrders() {
const [data, setData] = useState<PageResult<FulfillmentOrder>>(emptyPage)
const [merchants, setMerchants] = useState<Merchant[]>([])
const [loading, setLoading] = useState(false)
const [page, setPage] = useState(1)
const [size, setSize] = useState(20)
// 已提交的筛选条件即唯一数据源,由 effect 驱动查询;
// 关键字另存草稿,避免每敲一个字就打一次接口。
const [filters, setFilters] = useState<OrderFilters>({})
const [keywordDraft, setKeywordDraft] = useState('')
const load = useCallback(async () => {
const params: Record<string, unknown> = { page, size }
if (filters.merchantId) params.merchant_id = filters.merchantId
if (filters.orderStatus) params.order_status = filters.orderStatus
if (filters.orderSource) params.order_source = filters.orderSource
if (filters.keyword) params.keyword = filters.keyword
setLoading(true)
try {
setData(await platformApi.orders(params))
} catch (e) {
message.error(e instanceof Error ? e.message : '加载下游订单失败')
} finally {
setLoading(false)
}
}, [page, size, filters])
useEffect(() => {
void load()
}, [load])
useEffect(() => {
platformApi
.merchants({ page: 1, size: 200 })
.then((res) => setMerchants(res.list || []))
.catch(() => setMerchants([]))
}, [])
// 改筛选条件时回到第一页,避免停留在越界页码上看到空列表。
const updateFilters = useCallback((patch: Partial<OrderFilters>) => {
setPage(1)
setFilters((prev) => ({ ...prev, ...patch }))
}, [])
const submitKeyword = useCallback(() => {
updateFilters({ keyword: keywordDraft.trim() || undefined })
}, [keywordDraft, updateFilters])
const columns: ColumnsType<FulfillmentOrder> = useMemo(
() => [
{
title: '所属商户',
dataIndex: 'merchant_id',
width: 220,
render: (_, record) => (
<Space direction="vertical" size={0}>
<Text strong>{record.merchant?.name || `商户 #${record.merchant_id}`}</Text>
<Text type="secondary" style={{ fontSize: 12 }}>{record.merchant?.code || '-'}</Text>
</Space>
),
},
{
title: '订单号 / 商户单号',
dataIndex: 'order_no',
width: 260,
render: (_, record) => (
<Space direction="vertical" size={0}>
<Text copyable={{ text: record.order_no }} strong style={{ fontFamily: 'monospace', fontSize: 12 }}>
{record.order_no}
</Text>
<Text type="secondary" style={{ fontSize: 12, fontFamily: 'monospace' }}>
{record.client_order_no || '-'}
</Text>
</Space>
),
},
{
title: '商品',
dataIndex: 'product_name',
width: 190,
render: (_, record) => (
<Space direction="vertical" size={0}>
<Text>{record.product_name}</Text>
<Text type="secondary" style={{ fontSize: 12 }}>{record.product_sku}</Text>
</Space>
),
},
{
title: '金额',
dataIndex: 'amount',
width: 120,
align: 'right',
render: (value) => money(value),
},
{
title: '状态',
dataIndex: 'order_status',
width: 100,
render: (value) => orderStatusTag(value),
},
{
title: '来源',
dataIndex: 'order_source',
width: 100,
render: (value) => <Tag>{sourceText[value] || value}</Tag>,
},
{
title: '创建时间',
dataIndex: 'created_at',
width: 175,
render: formatDateTime,
},
{
title: '上游单号',
dataIndex: 'provider_order_no',
width: 200,
render: (value) => value
? <Text copyable={{ text: value }} style={{ fontFamily: 'monospace', fontSize: 12 }}>{value}</Text>
: <Text type="secondary">-</Text>,
},
],
[],
)
return (
<div>
<PageHeader
title="下游订单"
subtitle="平台视角:跨全部下游商户,不随当前商户上下文变化。商户后台的「发货订单」只显示单个商户。"
breadcrumbs={[{ title: '下游订单' }]}
extra={
<Button icon={<ReloadOutlined />} loading={loading} onClick={() => void load()}>
刷新
</Button>
}
/>
<Card style={{ marginBottom: 16, borderRadius: 10, border: '1px solid #e2e8f0' }} styles={{ body: { padding: '16px 20px' } }}>
<Row gutter={[12, 12]} align="middle">
<Col xs={24} md={6}>
<Select
allowClear
style={{ width: '100%' }}
placeholder="全部商户"
value={filters.merchantId}
onChange={(value) => updateFilters({ merchantId: value })}
options={merchants.map((item) => ({ value: item.id, label: `${item.name}(${item.code})` }))}
/>
</Col>
<Col xs={24} md={6}>
<Select
style={{ width: '100%' }}
value={filters.orderStatus || ''}
onChange={(value) => updateFilters({ orderStatus: value || undefined })}
options={orderStatusOptions.map((item) => ({
value: item.value,
label: item.value === '' ? '全部状态' : item.label,
}))}
/>
</Col>
<Col xs={24} md={5}>
<Select
style={{ width: '100%' }}
value={filters.orderSource || ''}
onChange={(value) => updateFilters({ orderSource: value || undefined })}
options={sourceOptions}
/>
</Col>
<Col xs={24} md={7}>
<Space.Compact style={{ width: '100%' }}>
<Input
allowClear
placeholder="订单号 / 商户单号"
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
value={keywordDraft}
onChange={(e) => setKeywordDraft(e.target.value)}
onPressEnter={submitKeyword}
/>
<Button type="primary" onClick={submitKeyword}>查询</Button>
</Space.Compact>
</Col>
</Row>
</Card>
<Card style={{ borderRadius: 10, border: '1px solid #e2e8f0' }} styles={{ body: { padding: 0 } }}>
<Table
rowKey="id"
loading={loading}
columns={columns}
dataSource={data.list}
locale={{ emptyText: <Empty description="没有符合条件的下游订单" /> }}
scroll={{ x: 1370 }}
pagination={pageConfig(data, (nextPage, nextSize) => {
setPage(nextPage)
setSize(nextSize)
})}
/>
</Card>
</div>
)
}
+6
View File
@@ -21,6 +21,9 @@ export interface Merchant {
created_at: string
}
// 平台侧跨商户订单列表里标识订单归属所需的商户信息(后端 Preload 的 Merchant 子集)
export type MerchantSummary = Pick<Merchant, 'id' | 'code' | 'name' | 'status'>
export interface MerchantMember {
id: number
merchant_id: number
@@ -132,6 +135,9 @@ export interface WalletLedgerEntry {
export interface FulfillmentOrder {
id: number
merchant_id: number
// merchant 仅平台侧跨商户列表会返回;商户侧响应不含该字段(后端 omitempty)
merchant?: MerchantSummary | null
order_no: string
client_order_no: string
product_sku: string