feat(finance): P2 手动退款登记与成本异常闭环
- 迁移 112:order_refunds(订单项级退款,整单拆多条共享 batch_no,source/external_ref 预留平台 API)+ cost_exceptions.resolution_note - 新服务 order-refund-service:整单/指定商品退款、可退金额校验、撤销、全额退完同步 pay_status - 销售事实改为按有效退款记录冲减;无记录时仍兼容 pay_status=refunded 的历史全额退款 - 订单详情新增退款登记与退款记录表,商品明细显示收入/已退/可退 - 全额退款自动关闭该订单项成本异常并写处理备注;成本异常处理改为带备注弹窗 - 财务事件 UNION 接入退款记录,文档同步 P2 状态与 API
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
FileTextOutlined,
|
||||
PauseCircleOutlined,
|
||||
PlayCircleOutlined,
|
||||
RollbackOutlined,
|
||||
StopOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
@@ -11,9 +12,14 @@ import {
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
DatePicker,
|
||||
Descriptions,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Radio,
|
||||
Select,
|
||||
Skeleton,
|
||||
Space,
|
||||
Tag,
|
||||
@@ -22,19 +28,29 @@ import {
|
||||
import PercentTable from '@/components/common/PercentTable'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router'
|
||||
|
||||
import dayjs from '@/lib/dayjs'
|
||||
|
||||
import JsonPreview from '@/components/admin/JsonPreview'
|
||||
import ManualOrderCostModal from '@/components/admin/ManualOrderCostModal'
|
||||
import PageHeader from '@/components/admin/PageHeader'
|
||||
import StatusTag from '@/components/admin/StatusTag'
|
||||
import { fetchAdminOrderDetail, updateAdminOrderClaimStatus } from '@/services/admin'
|
||||
import type { AdminOrderDetail, AdminTaskListItem } from '@/types/admin'
|
||||
import {
|
||||
createAdminOrderRefund,
|
||||
fetchAdminOrderDetail,
|
||||
updateAdminOrderClaimStatus,
|
||||
voidAdminOrderRefund,
|
||||
} from '@/services/admin'
|
||||
import type { AdminOrderDetail, AdminOrderRefund, AdminTaskListItem } from '@/types/admin'
|
||||
import { hasAdminPermission } from '@/utils/admin-auth'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { resolveOrderFulfillmentMode, resolveVoucherStateLabel } from '@/utils/admin-display'
|
||||
|
||||
import { CostPreviewModal } from './cost-center/CostPreviewModal'
|
||||
import { formatMoney } from './panels/shared'
|
||||
|
||||
export default function AdminOrderDetailPage() {
|
||||
const navigate = useNavigate()
|
||||
@@ -45,6 +61,17 @@ export default function AdminOrderDetailPage() {
|
||||
const [statusModalVisible, setStatusModalVisible] = useState(false)
|
||||
const [manualCostOpen, setManualCostOpen] = useState(false)
|
||||
const [costPreviewItemId, setCostPreviewItemId] = useState<number | null>(null)
|
||||
const [refundModalOpen, setRefundModalOpen] = useState(false)
|
||||
const [refundScope, setRefundScope] = useState<'order' | 'item'>('order')
|
||||
const [refundItemId, setRefundItemId] = useState<number | null>(null)
|
||||
const [refundAmountYuan, setRefundAmountYuan] = useState<number | null>(null)
|
||||
const [refundReason, setRefundReason] = useState('买家退款')
|
||||
const [refundNote, setRefundNote] = useState('')
|
||||
const [refundedAt, setRefundedAt] = useState<Dayjs>(dayjs())
|
||||
const [refunding, setRefunding] = useState(false)
|
||||
const [voidTarget, setVoidTarget] = useState<AdminOrderRefund | null>(null)
|
||||
const [voidReason, setVoidReason] = useState('')
|
||||
const [voiding, setVoiding] = useState(false)
|
||||
const [targetStatus, setTargetStatus] = useState<'active' | 'paused' | 'closed'>('active')
|
||||
const [statusReason, setStatusReason] = useState('')
|
||||
|
||||
@@ -112,6 +139,77 @@ export default function AdminOrderDetailPage() {
|
||||
setStatusModalVisible(true)
|
||||
}
|
||||
|
||||
function openRefundModal(itemId?: number) {
|
||||
const items = detail?.items || []
|
||||
const target = itemId ? items.find((item) => item.orderItemId === itemId) : null
|
||||
const fallback = items.find((item) => item.refundableFen > 0) || null
|
||||
const focus = target || fallback
|
||||
setRefundScope(target ? 'item' : 'order')
|
||||
setRefundItemId(focus?.orderItemId ?? null)
|
||||
setRefundAmountYuan(focus ? focus.refundableFen / 100 : 0)
|
||||
setRefundReason('买家退款')
|
||||
setRefundNote('')
|
||||
setRefundedAt(dayjs())
|
||||
setRefundModalOpen(true)
|
||||
}
|
||||
|
||||
async function submitRefund() {
|
||||
if (!detail) return
|
||||
if (refundScope === 'item' && !refundItemId) {
|
||||
message.warning('请选择要退款的商品')
|
||||
return
|
||||
}
|
||||
const amountFen = Math.round(Number(refundAmountYuan || 0) * 100)
|
||||
if (refundScope === 'item' && amountFen <= 0) {
|
||||
message.warning('请输入退款金额')
|
||||
return
|
||||
}
|
||||
setRefunding(true)
|
||||
try {
|
||||
const response = await createAdminOrderRefund(detail.order.orderId, {
|
||||
orderItemId: refundScope === 'item' ? refundItemId || undefined : undefined,
|
||||
amountFen: refundScope === 'item' ? amountFen : undefined,
|
||||
reason: refundReason.trim() || '手动退款',
|
||||
note: refundNote.trim(),
|
||||
refundedAt: refundedAt.format('YYYY-MM-DD HH:mm:ss'),
|
||||
})
|
||||
const result = response.data
|
||||
message.success(
|
||||
`已登记 ${result.refunds.length} 条退款${result.fullRefunded ? '(订单已全额退款)' : ''}${
|
||||
result.closedExceptionCount > 0
|
||||
? `,自动关闭 ${result.closedExceptionCount} 条成本异常`
|
||||
: ''
|
||||
}`,
|
||||
)
|
||||
setRefundModalOpen(false)
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-order-detail', orderId] })
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-finance'] })
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '登记退款失败')
|
||||
} finally {
|
||||
setRefunding(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function submitVoidRefund() {
|
||||
if (!detail || !voidTarget) return
|
||||
setVoiding(true)
|
||||
try {
|
||||
await voidAdminOrderRefund(detail.order.orderId, voidTarget.id, {
|
||||
reason: voidReason.trim() || '撤销退款',
|
||||
})
|
||||
message.success('退款已撤销')
|
||||
setVoidTarget(null)
|
||||
setVoidReason('')
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-order-detail', orderId] })
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-finance'] })
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '撤销退款失败')
|
||||
} finally {
|
||||
setVoiding(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (query.isLoading) {
|
||||
return (
|
||||
<section className="page-stack">
|
||||
@@ -206,6 +304,16 @@ export default function AdminOrderDetailPage() {
|
||||
title="基础信息"
|
||||
extra={
|
||||
<Space>
|
||||
{hasAdminPermission('finance.config') ? (
|
||||
<Button
|
||||
danger
|
||||
icon={<RollbackOutlined />}
|
||||
disabled={!detail.items.some((item) => item.refundableFen > 0)}
|
||||
onClick={() => openRefundModal()}
|
||||
>
|
||||
退款登记
|
||||
</Button>
|
||||
) : null}
|
||||
<Button icon={<FileTextOutlined />} onClick={() => setManualCostOpen(true)}>
|
||||
人工录单
|
||||
</Button>
|
||||
@@ -351,6 +459,18 @@ export default function AdminOrderDetailPage() {
|
||||
),
|
||||
},
|
||||
{ title: '数量', dataIndex: 'quantity', width: 90 },
|
||||
{
|
||||
title: '收入 / 已退',
|
||||
width: 190,
|
||||
render: (_, item) => (
|
||||
<div className="cell-stack" style={{ gap: 4 }}>
|
||||
<span>{formatMoney(item.revenueFen)}</span>
|
||||
<span className="muted">
|
||||
已退 {formatMoney(item.refundedFen)} · 可退 {formatMoney(item.refundableFen)}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ title: '履约模式', dataIndex: 'deliveryMode', minWidth: 140 },
|
||||
{
|
||||
title: '成本',
|
||||
@@ -365,6 +485,99 @@ export default function AdminOrderDetailPage() {
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{detail.refunds.length > 0 ? (
|
||||
<Card
|
||||
title={
|
||||
<Space size={6}>
|
||||
<Typography.Text strong>退款记录</Typography.Text>
|
||||
<Tag color="red">
|
||||
{formatMoney(
|
||||
detail.refunds
|
||||
.filter((item) => item.status === 'confirmed')
|
||||
.reduce((sum, item) => sum + item.amountFen, 0),
|
||||
)}
|
||||
</Tag>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<PercentTable<AdminOrderRefund>
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
dataSource={detail.refunds}
|
||||
scroll={{ x: 980 }}
|
||||
columns={[
|
||||
{
|
||||
title: '退款单号',
|
||||
dataIndex: 'refundNo',
|
||||
width: 190,
|
||||
render: (value: string, row) => (
|
||||
<div className="cell-stack" style={{ gap: 2 }}>
|
||||
<span>{value}</span>
|
||||
<span className="muted">{row.batchNo}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '商品',
|
||||
dataIndex: 'skuName',
|
||||
width: 240,
|
||||
render: (value: string) => value || '-',
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'amountFen',
|
||||
width: 120,
|
||||
render: (value: number) => formatMoney(value),
|
||||
},
|
||||
{
|
||||
title: '原因',
|
||||
dataIndex: 'reason',
|
||||
width: 220,
|
||||
render: (value: string, row) => (
|
||||
<div className="cell-stack" style={{ gap: 2 }}>
|
||||
<span>{value || '-'}</span>
|
||||
{row.note ? <span className="muted">{row.note}</span> : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '退款时间',
|
||||
dataIndex: 'refundedAt',
|
||||
width: 180,
|
||||
render: (value: string) => formatAdminDateTime(value),
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
dataIndex: 'operatorName',
|
||||
width: 120,
|
||||
render: (value: string) => value || '-',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
render: (value: string, row) =>
|
||||
value === 'confirmed' ? (
|
||||
<Tag color="green">已确认</Tag>
|
||||
) : (
|
||||
<Tag color="default">已撤销{row.voidReason ? `:${row.voidReason}` : ''}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 90,
|
||||
render: (_, row) =>
|
||||
hasAdminPermission('finance.config') && row.status === 'confirmed' ? (
|
||||
<Button size="small" danger onClick={() => setVoidTarget(row)}>
|
||||
撤销
|
||||
</Button>
|
||||
) : null,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<Card title="任务">
|
||||
<PercentTable<AdminTaskListItem>
|
||||
rowKey="taskId"
|
||||
@@ -421,6 +634,123 @@ export default function AdminOrderDetailPage() {
|
||||
onClose={closeManualCost}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="退款登记"
|
||||
open={refundModalOpen}
|
||||
onCancel={() => setRefundModalOpen(false)}
|
||||
onOk={() => void submitRefund()}
|
||||
confirmLoading={refunding}
|
||||
okText="确认退款"
|
||||
cancelText="取消"
|
||||
width={560}
|
||||
>
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={`当前可退合计 ${formatMoney(
|
||||
detail.items.reduce((sum, item) => sum + item.refundableFen, 0),
|
||||
)}`}
|
||||
description="整单退款会按每个商品剩余可退金额拆成多条记录;登记后当天销售事实立即重算。"
|
||||
/>
|
||||
<Radio.Group
|
||||
value={refundScope}
|
||||
onChange={(event) => {
|
||||
const next = event.target.value as 'order' | 'item'
|
||||
setRefundScope(next)
|
||||
if (next === 'item') {
|
||||
const focus = detail.items.find((item) => item.refundableFen > 0) || null
|
||||
setRefundItemId(focus?.orderItemId ?? null)
|
||||
setRefundAmountYuan(focus ? focus.refundableFen / 100 : 0)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Radio.Button value="order">整单退款</Radio.Button>
|
||||
<Radio.Button value="item">指定商品</Radio.Button>
|
||||
</Radio.Group>
|
||||
{refundScope === 'item' ? (
|
||||
<>
|
||||
<Select
|
||||
value={refundItemId ?? undefined}
|
||||
onChange={(value) => {
|
||||
setRefundItemId(value)
|
||||
const item = detail.items.find((row) => row.orderItemId === value)
|
||||
setRefundAmountYuan(item ? item.refundableFen / 100 : 0)
|
||||
}}
|
||||
options={detail.items
|
||||
.filter((item) => item.refundableFen > 0)
|
||||
.map((item) => ({
|
||||
value: item.orderItemId,
|
||||
label: `${item.itemTitle || item.skuName} · 可退 ${formatMoney(item.refundableFen)}`,
|
||||
}))}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<InputNumber
|
||||
addonBefore="¥"
|
||||
min={0.01}
|
||||
precision={2}
|
||||
value={refundAmountYuan}
|
||||
onChange={(value) => setRefundAmountYuan(value ?? 0)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Typography.Text type="secondary">
|
||||
将按 {detail.items.filter((item) => item.refundableFen > 0).length}{' '}
|
||||
个有可退金额的商品逐条登记。
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Input
|
||||
value={refundReason}
|
||||
onChange={(event) => setRefundReason(event.target.value)}
|
||||
placeholder="退款原因,如:买家退款 / 售后补偿 / 发错货"
|
||||
/>
|
||||
<DatePicker
|
||||
showTime
|
||||
value={refundedAt}
|
||||
onChange={(value) => {
|
||||
if (value) setRefundedAt(value)
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Input.TextArea
|
||||
rows={2}
|
||||
value={refundNote}
|
||||
onChange={(event) => setRefundNote(event.target.value)}
|
||||
placeholder="备注(可选,如平台退款单号、沟通记录)"
|
||||
/>
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="撤销退款"
|
||||
open={voidTarget !== null}
|
||||
onCancel={() => {
|
||||
setVoidTarget(null)
|
||||
setVoidReason('')
|
||||
}}
|
||||
onOk={() => void submitVoidRefund()}
|
||||
confirmLoading={voiding}
|
||||
okText="确认撤销"
|
||||
okButtonProps={{ danger: true }}
|
||||
cancelText="取消"
|
||||
width={520}
|
||||
>
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message={`撤销 ${voidTarget?.refundNo || ''}(${formatMoney(voidTarget?.amountFen || 0)})`}
|
||||
description="撤销后销售事实会按剩余有效退款重算;已置为「已退款」的订单支付状态不会自动回退。"
|
||||
/>
|
||||
<Input
|
||||
value={voidReason}
|
||||
onChange={(event) => setVoidReason(event.target.value)}
|
||||
placeholder="撤销原因(可选)"
|
||||
/>
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<CostPreviewModal
|
||||
open={costPreviewItemId !== null}
|
||||
orderItemId={costPreviewItemId}
|
||||
|
||||
@@ -5,7 +5,21 @@
|
||||
* 页面先按原因聚合,默认只看近 24 小时,历史存量需要显式切换时间范围。
|
||||
* 每行可直接打开「成本解析」定位是哪个 SKU 没归一、哪条成本没命中。
|
||||
*/
|
||||
import { App, Button, Card, Col, Row, Select, Space, Statistic, Tag, Typography } from 'antd'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import PercentTable from '@/components/common/PercentTable'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
@@ -50,7 +64,7 @@ const RANGE_OPTIONS = [
|
||||
]
|
||||
|
||||
export function CostExceptionPanel() {
|
||||
const { message, modal } = App.useApp()
|
||||
const { message } = App.useApp()
|
||||
const { canConfig, fulfillmentModeFilter, refreshAll } = useCostCenter()
|
||||
|
||||
const [status, setStatus] = useState('open')
|
||||
@@ -59,6 +73,12 @@ export function CostExceptionPanel() {
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
||||
const [previewItemId, setPreviewItemId] = useState<number | null>(null)
|
||||
const [resolveTarget, setResolveTarget] = useState<{
|
||||
item: CostException
|
||||
status: 'resolved' | 'ignored'
|
||||
} | null>(null)
|
||||
const [resolveNote, setResolveNote] = useState('')
|
||||
const [resolving, setResolving] = useState(false)
|
||||
|
||||
// 时间下界在取数时计算,避免渲染期调用 Date.now(React 纯度规则)。
|
||||
const summaryQuery = useQuery({
|
||||
@@ -102,22 +122,28 @@ export function CostExceptionPanel() {
|
||||
const exceptions = exceptionsQuery.data?.data.items || []
|
||||
const total = exceptionsQuery.data?.data.pagination.total || 0
|
||||
|
||||
function resolve(item: CostException, nextStatus: 'resolved' | 'ignored') {
|
||||
modal.confirm({
|
||||
title: nextStatus === 'resolved' ? '标记异常已处理' : '忽略异常',
|
||||
content: `确认处理「${REASON_LABELS[item.reason] || item.reason}」?`,
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await resolveAdminCostException(item.id, { status: nextStatus })
|
||||
message.success('成本异常已处理')
|
||||
refreshAll()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '处理成本异常失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
function openResolve(item: CostException, nextStatus: 'resolved' | 'ignored') {
|
||||
setResolveNote('')
|
||||
setResolveTarget({ item, status: nextStatus })
|
||||
}
|
||||
|
||||
async function submitResolve() {
|
||||
if (!resolveTarget) return
|
||||
setResolving(true)
|
||||
try {
|
||||
await resolveAdminCostException(resolveTarget.item.id, {
|
||||
status: resolveTarget.status,
|
||||
note: resolveNote.trim(),
|
||||
})
|
||||
message.success('成本异常已处理')
|
||||
setResolveTarget(null)
|
||||
setResolveNote('')
|
||||
refreshAll()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '处理成本异常失败')
|
||||
} finally {
|
||||
setResolving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<CostException> = [
|
||||
@@ -131,6 +157,11 @@ export function CostExceptionPanel() {
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{String(row.detail?.message || '')}
|
||||
</Typography.Text>
|
||||
{row.status !== 'open' && row.resolutionNote ? (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
处理备注:{row.resolutionNote}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
@@ -183,10 +214,10 @@ export function CostExceptionPanel() {
|
||||
</Button>
|
||||
{canConfig && row.status === 'open' ? (
|
||||
<>
|
||||
<Button size="small" type="primary" onClick={() => resolve(row, 'resolved')}>
|
||||
<Button size="small" type="primary" onClick={() => openResolve(row, 'resolved')}>
|
||||
已处理
|
||||
</Button>
|
||||
<Button size="small" onClick={() => resolve(row, 'ignored')}>
|
||||
<Button size="small" onClick={() => openResolve(row, 'ignored')}>
|
||||
忽略
|
||||
</Button>
|
||||
</>
|
||||
@@ -308,6 +339,39 @@ export function CostExceptionPanel() {
|
||||
})}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={resolveTarget?.status === 'ignored' ? '忽略成本异常' : '标记异常已处理'}
|
||||
open={resolveTarget !== null}
|
||||
onCancel={() => setResolveTarget(null)}
|
||||
onOk={() => void submitResolve()}
|
||||
confirmLoading={resolving}
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
width={520}
|
||||
>
|
||||
<Space direction="vertical" size={10} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={
|
||||
resolveTarget
|
||||
? REASON_LABELS[resolveTarget.item.reason] || resolveTarget.item.reason
|
||||
: ''
|
||||
}
|
||||
description={String(resolveTarget?.item.detail?.message || '')}
|
||||
/>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
value={resolveNote}
|
||||
onChange={(event) => setResolveNote(event.target.value)}
|
||||
placeholder="处理备注(可选):怎么处理的、为什么忽略"
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
备注会随异常一起留档;退款导致的异常由系统自动写入备注。
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<CostPreviewModal
|
||||
open={previewItemId !== null}
|
||||
orderItemId={previewItemId}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type { AdminOrderDetail, AdminOrderListItem, AdminPagination } from '@/types/admin'
|
||||
import type {
|
||||
AdminOrderDetail,
|
||||
AdminOrderListItem,
|
||||
AdminOrderRefund,
|
||||
AdminPagination,
|
||||
} from '@/types/admin'
|
||||
|
||||
export function fetchAdminOrders(params?: Record<string, unknown>) {
|
||||
return apiGet<{ items: AdminOrderListItem[]; pagination: AdminPagination }>(
|
||||
@@ -24,3 +29,32 @@ export function updateAdminOrderClaimStatus(
|
||||
claimStatusUpdatedAt: string
|
||||
}>(`/api/v1/admin/orders/${orderId}/claim-status`, payload)
|
||||
}
|
||||
|
||||
export function createAdminOrderRefund(
|
||||
orderId: number | string,
|
||||
payload: {
|
||||
orderItemId?: number
|
||||
amountFen?: number
|
||||
reason?: string
|
||||
note?: string
|
||||
refundedAt?: string
|
||||
},
|
||||
) {
|
||||
return apiPost<{
|
||||
batchNo: string
|
||||
refunds: AdminOrderRefund[]
|
||||
fullRefunded: boolean
|
||||
closedExceptionCount: number
|
||||
}>(`/api/v1/admin/orders/${orderId}/refunds`, payload)
|
||||
}
|
||||
|
||||
export function voidAdminOrderRefund(
|
||||
orderId: number | string,
|
||||
refundId: number,
|
||||
payload: { reason?: string },
|
||||
) {
|
||||
return apiPost<{ refund: AdminOrderRefund; rebuiltDate: string }>(
|
||||
`/api/v1/admin/orders/${orderId}/refunds/${refundId}/void`,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -138,6 +138,7 @@ export interface CostException {
|
||||
createdAt: string
|
||||
resolvedAt: string | null
|
||||
resolvedByAdminUserId: number | null
|
||||
resolutionNote: string
|
||||
}
|
||||
|
||||
export interface CostCenterOptions {
|
||||
|
||||
@@ -58,7 +58,12 @@ export type {
|
||||
export type { AdminLoginLogItem, AdminLoginLogListResult } from './login-logs'
|
||||
|
||||
// Orders types
|
||||
export type { AdminOrderFulfillmentProgress, AdminOrderListItem, AdminOrderDetail } from './orders'
|
||||
export type {
|
||||
AdminOrderFulfillmentProgress,
|
||||
AdminOrderListItem,
|
||||
AdminOrderDetail,
|
||||
AdminOrderRefund,
|
||||
} from './orders'
|
||||
|
||||
// Tasks types
|
||||
export type {
|
||||
|
||||
@@ -112,9 +112,36 @@ export interface AdminOrderDetail {
|
||||
skuNick?: string
|
||||
matchSku?: string
|
||||
quantity: number
|
||||
/** 订单项收入(分) */
|
||||
revenueFen: number
|
||||
/** 已确认退款(分) */
|
||||
refundedFen: number
|
||||
/** 还可退金额(分) */
|
||||
refundableFen: number
|
||||
fulfillmentMode: AdminOrderItemFulfillmentMode
|
||||
deliveryMode: string
|
||||
spec: Record<string, unknown>
|
||||
}>
|
||||
refunds: AdminOrderRefund[]
|
||||
tasks: AdminTaskListItem[]
|
||||
}
|
||||
|
||||
export interface AdminOrderRefund {
|
||||
id: number
|
||||
refundNo: string
|
||||
batchNo: string
|
||||
orderId: number
|
||||
orderItemId: number
|
||||
skuName: string
|
||||
amountFen: number
|
||||
reason: string
|
||||
note: string
|
||||
source: string
|
||||
status: 'confirmed' | 'voided'
|
||||
refundedAt: string
|
||||
operatorAdminUserId: number | null
|
||||
operatorName: string
|
||||
voidedAt: string | null
|
||||
voidReason: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user