From 8c12d1d448ca1402bca2187d5cac2ee2e6cfb3ac Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 6 Oct 2026 20:28:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E8=B6=85=E7=AE=A1=E7=9C=8B?= =?UTF-8?q?=E4=B8=8D=E5=88=B0=E4=B8=8B=E6=B8=B8=E8=AE=A2=E5=8D=95=E2=80=94?= =?UTF-8?q?=E2=80=94=E6=96=B0=E5=A2=9E=E5=B9=B3=E5=8F=B0=E4=BE=A7=E8=B7=A8?= =?UTF-8?q?=E5=95=86=E6=88=B7=E8=AE=A2=E5=8D=95=E8=A7=86=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:管理员首页 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 字段。 --- README.md | 16 ++ backend/internal/handler/merchant_order.go | 32 +++ backend/internal/model/merchant.go | 3 + backend/internal/router/router.go | 3 + .../fulfillment_platform_orders_test.go | 135 ++++++++++ backend/internal/service/fulfillment_query.go | 74 +++++- frontend/src/App.tsx | 9 + frontend/src/api/index.ts | 3 + frontend/src/layouts/MainLayout.tsx | 5 +- frontend/src/pages/PlatformOrders.tsx | 250 ++++++++++++++++++ frontend/src/types/index.ts | 6 + 11 files changed, 522 insertions(+), 14 deletions(-) create mode 100644 backend/internal/service/fulfillment_platform_orders_test.go create mode 100644 frontend/src/pages/PlatformOrders.tsx diff --git a/README.md b/README.md index d18adff..625b3da 100644 --- a/README.md +++ b/README.md @@ -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`,只会在「平台商户」页点「成员」时被写入。 +需要看某个下游商户的订单时,用「下游订单」页的商户筛选即可,不必切换上下文。 + ## 目录结构 ``` diff --git a/backend/internal/handler/merchant_order.go b/backend/internal/handler/merchant_order.go index e7dd175..3863648 100644 --- a/backend/internal/handler/merchant_order.go +++ b/backend/internal/handler/merchant_order.go @@ -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"` diff --git a/backend/internal/model/merchant.go b/backend/internal/model/merchant.go index 262a668..e019214 100644 --- a/backend/internal/model/merchant.go +++ b/backend/internal/model/merchant.go @@ -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 为商户提供独立、可禁用的事件订阅。 diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index e365cd4..429cd5c 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -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) diff --git a/backend/internal/service/fulfillment_platform_orders_test.go b/backend/internal/service/fulfillment_platform_orders_test.go new file mode 100644 index 0000000..f8acbd7 --- /dev/null +++ b/backend/internal/service/fulfillment_platform_orders_test.go @@ -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,否则会改变既有响应结构") + } + } +} diff --git a/backend/internal/service/fulfillment_query.go b/backend/internal/service/fulfillment_query.go index 31fb4ca..008bf13 100644 --- a/backend/internal/service/fulfillment_query.go +++ b/backend/internal/service/fulfillment_query.go @@ -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"` diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1c3e8c0..4312bbf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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() { } /> } /> } /> + + + + } + /> r.data.data), merchants: (params?: Record) => request.get('/platform/merchants', { params }).then((r) => r.data.data as PageResult), + // 平台侧跨商户订单视图:不传 merchant_id 即全部商户 + orders: (params?: Record) => + request.get('/platform/orders', { params }).then((r) => r.data.data as PageResult), createMerchant: (data: { code: string name: string diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx index 263296f..ea0c7d8 100644 --- a/frontend/src/layouts/MainLayout.tsx +++ b/frontend/src/layouts/MainLayout.tsx @@ -88,7 +88,10 @@ const adminSections: SidebarSection[] = [ key: 'orders', label: '订单管理', icon: , - 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', diff --git a/frontend/src/pages/PlatformOrders.tsx b/frontend/src/pages/PlatformOrders.tsx new file mode 100644 index 0000000..1b5e05d --- /dev/null +++ b/frontend/src/pages/PlatformOrders.tsx @@ -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 = { + api: '开放接口', + manual: '人工下单', + test: '联调测试', +} + +interface OrderFilters { + merchantId?: number + orderStatus?: string + orderSource?: string + keyword?: string +} + +const emptyPage: PageResult = { list: [], total: 0, page: 1, size: 20 } + +function orderStatusTag(value: string) { + const item = orderStatusMap[value] || { color: 'default', text: value } + return {item.text} +} + +/** + * 平台侧「下游订单」页。 + * + * 存在的理由:商户后台的「发货订单」走 /api/merchant/orders,服务端强制按单个商户隔离, + * 而管理员的默认商户上下文是自营商户——于是首页 Dashboard 显示平台总量(跨商户), + * 订单页却只显示自营商户的量,两个数字对不上且无法解释。本页走 /api/platform/orders, + * 不依赖 X-Merchant-ID,管理员可以在这里看到全部下游商户的订单。 + */ +export default function PlatformOrders() { + const [data, setData] = useState>(emptyPage) + const [merchants, setMerchants] = useState([]) + const [loading, setLoading] = useState(false) + + const [page, setPage] = useState(1) + const [size, setSize] = useState(20) + // 已提交的筛选条件即唯一数据源,由 effect 驱动查询; + // 关键字另存草稿,避免每敲一个字就打一次接口。 + const [filters, setFilters] = useState({}) + const [keywordDraft, setKeywordDraft] = useState('') + + const load = useCallback(async () => { + const params: Record = { 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) => { + setPage(1) + setFilters((prev) => ({ ...prev, ...patch })) + }, []) + + const submitKeyword = useCallback(() => { + updateFilters({ keyword: keywordDraft.trim() || undefined }) + }, [keywordDraft, updateFilters]) + + const columns: ColumnsType = useMemo( + () => [ + { + title: '所属商户', + dataIndex: 'merchant_id', + width: 220, + render: (_, record) => ( + + {record.merchant?.name || `商户 #${record.merchant_id}`} + {record.merchant?.code || '-'} + + ), + }, + { + title: '订单号 / 商户单号', + dataIndex: 'order_no', + width: 260, + render: (_, record) => ( + + + {record.order_no} + + + {record.client_order_no || '-'} + + + ), + }, + { + title: '商品', + dataIndex: 'product_name', + width: 190, + render: (_, record) => ( + + {record.product_name} + {record.product_sku} + + ), + }, + { + 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) => {sourceText[value] || value}, + }, + { + title: '创建时间', + dataIndex: 'created_at', + width: 175, + render: formatDateTime, + }, + { + title: '上游单号', + dataIndex: 'provider_order_no', + width: 200, + render: (value) => value + ? {value} + : -, + }, + ], + [], + ) + + return ( +
+ } loading={loading} onClick={() => void load()}> + 刷新 + + } + /> + + + + + updateFilters({ orderStatus: value || undefined })} + options={orderStatusOptions.map((item) => ({ + value: item.value, + label: item.value === '' ? '全部状态' : item.label, + }))} + /> + + + } + value={keywordDraft} + onChange={(e) => setKeywordDraft(e.target.value)} + onPressEnter={submitKeyword} + /> + + + + + + + + }} + scroll={{ x: 1370 }} + pagination={pageConfig(data, (nextPage, nextSize) => { + setPage(nextPage) + setSize(nextSize) + })} + /> + + + ) +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index e9e4582..301a75d 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -21,6 +21,9 @@ export interface Merchant { created_at: string } +// 平台侧跨商户订单列表里标识订单归属所需的商户信息(后端 Preload 的 Merchant 子集) +export type MerchantSummary = Pick + 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