feat(worker-platform): 支持客户退款冲正并关闭订单
This commit is contained in:
@@ -71,9 +71,11 @@ import {
|
||||
fetchAdminWorkerProductNameSuffix,
|
||||
fetchAdminWorkerUsers,
|
||||
fetchAdminWorkOrders,
|
||||
executeAdminCustomerRefund,
|
||||
markAdminWorkOrderProblem,
|
||||
markAdminWorkOrderShareProblem,
|
||||
pinAdminWorkOrder,
|
||||
previewAdminCustomerRefund,
|
||||
publishAdminWorkOrder,
|
||||
reduceAdminWorkOrderShareQuantity,
|
||||
reopenAdminCancelledWorkOrder,
|
||||
@@ -86,6 +88,7 @@ import {
|
||||
updateAdminWorkOrderNote,
|
||||
updateAdminWorkOrderSharing,
|
||||
} from '@/services/admin'
|
||||
import type { CustomerRefundPreview } from '@/services/admin/worker-platform'
|
||||
import {
|
||||
checkWorkOrderConsignmentMatches,
|
||||
fulfillWorkOrderWithConsignment,
|
||||
@@ -203,6 +206,11 @@ type CancelSharingPartFormValues = {
|
||||
reason?: string
|
||||
}
|
||||
|
||||
type CustomerRefundFormValues = {
|
||||
reason: string
|
||||
note?: string
|
||||
}
|
||||
|
||||
/** 工单 `material.consignment`(寄售履约信息);没有寄售履约信息时返回 null。 */
|
||||
function getConsignmentFulfillment(row: WorkOrder): ConsignmentFulfillmentInfo | null {
|
||||
const consignment = asRecord((row.material as Record<string, unknown> | undefined)?.consignment)
|
||||
@@ -304,6 +312,9 @@ export default function WorkOrdersPanel() {
|
||||
const canCancelOrder = sessionPermissions
|
||||
? sessionPermissions.includes('worker_order.cancel')
|
||||
: hasAdminPermission('worker_order.cancel')
|
||||
const canCustomerRefund = sessionPermissions
|
||||
? sessionPermissions.includes('worker_order.customer_refund')
|
||||
: hasAdminPermission('worker_order.customer_refund')
|
||||
const canCancelShare = sessionPermissions
|
||||
? sessionPermissions.includes('worker_order.cancel_share')
|
||||
: hasAdminPermission('worker_order.cancel_share')
|
||||
@@ -379,6 +390,12 @@ export default function WorkOrdersPanel() {
|
||||
const [noteOrder, setNoteOrder] = useState<WorkOrder | null>(null)
|
||||
const [deductOrder, setDeductOrder] = useState<WorkOrder | null>(null)
|
||||
const [cancelOrder, setCancelOrder] = useState<WorkOrder | null>(null)
|
||||
const [customerRefundOrder, setCustomerRefundOrder] = useState<WorkOrder | null>(null)
|
||||
const [customerRefundPreview, setCustomerRefundPreview] = useState<CustomerRefundPreview | null>(
|
||||
null,
|
||||
)
|
||||
const [customerRefundLoading, setCustomerRefundLoading] = useState(false)
|
||||
const [customerRefundSubmitting, setCustomerRefundSubmitting] = useState(false)
|
||||
const [problemForm] = Form.useForm()
|
||||
const [resolutionForm] = Form.useForm()
|
||||
const [materialForm] = Form.useForm<MaterialFormValues>()
|
||||
@@ -393,6 +410,7 @@ export default function WorkOrdersPanel() {
|
||||
const [cancelSharingForm] = Form.useForm<CancelSharingFormValues>()
|
||||
const [cancelSharingPartForm] = Form.useForm<CancelSharingPartFormValues>()
|
||||
const [deductForm] = Form.useForm<{ amount?: number; note?: string }>()
|
||||
const [customerRefundForm] = Form.useForm<CustomerRefundFormValues>()
|
||||
const [editForm] = Form.useForm<{
|
||||
productName?: string
|
||||
platformOrderId?: string
|
||||
@@ -692,6 +710,46 @@ export default function WorkOrdersPanel() {
|
||||
cancelForm.setFieldsValue({ action: 'return_to_hall', reason: '' })
|
||||
}
|
||||
|
||||
async function openCustomerRefundModal(row: WorkOrder) {
|
||||
setCustomerRefundOrder(row)
|
||||
setCustomerRefundPreview(null)
|
||||
customerRefundForm.setFieldsValue({ reason: '客户退款', note: '' })
|
||||
setCustomerRefundLoading(true)
|
||||
try {
|
||||
const response = await previewAdminCustomerRefund(row.workOrderId)
|
||||
setCustomerRefundPreview(response.data)
|
||||
} catch (error) {
|
||||
setCustomerRefundOrder(null)
|
||||
message.error(error instanceof Error ? error.message : '客户退款预检失败')
|
||||
} finally {
|
||||
setCustomerRefundLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCustomerRefund(values: CustomerRefundFormValues) {
|
||||
if (!customerRefundOrder || !customerRefundPreview?.canExecute) return
|
||||
setCustomerRefundSubmitting(true)
|
||||
try {
|
||||
const response = await executeAdminCustomerRefund(customerRefundOrder.workOrderId, values)
|
||||
message.success(
|
||||
response.data.idempotent
|
||||
? '该客户退款已处理,无需重复操作'
|
||||
: `客户退款已完成,退款 ${formatMoney(customerRefundPreview.remainingFen)}`,
|
||||
)
|
||||
setCustomerRefundOrder(null)
|
||||
setCustomerRefundPreview(null)
|
||||
customerRefundForm.resetFields()
|
||||
await refreshAll()
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-redeem-certificates'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-kuaishou-industry-vouchers'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['fulfillment-pending'] })
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '客户退款失败')
|
||||
} finally {
|
||||
setCustomerRefundSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
function applyMaterialPaste(text: string) {
|
||||
if (!materialOrder) return
|
||||
const recognized = parseWorkerMaterialText(text)
|
||||
@@ -1990,6 +2048,27 @@ export default function WorkOrdersPanel() {
|
||||
手动匹配寄售
|
||||
</Button>
|
||||
) : null}
|
||||
{canCustomerRefund &&
|
||||
[
|
||||
'pending_material',
|
||||
'unassigned',
|
||||
'open',
|
||||
'in_progress',
|
||||
'pending_acceptance',
|
||||
'problem',
|
||||
].includes(String(row.rawStatus || row.status || '')) ? (
|
||||
<Button
|
||||
danger
|
||||
icon={<RollbackOutlined />}
|
||||
loading={
|
||||
customerRefundLoading && customerRefundOrder?.workOrderId === row.workOrderId
|
||||
}
|
||||
onClick={() => void openCustomerRefundModal(row)}
|
||||
title="同意平台用户退款,并关闭工单、资料、领取链接和待核销任务"
|
||||
>
|
||||
客户退款
|
||||
</Button>
|
||||
) : null}
|
||||
{row.capabilities?.canCancel && (canReturnToHall || canCancelOrder) ? (
|
||||
<Button danger icon={<UndoOutlined />} onClick={() => openCancelModal(row)}>
|
||||
撤单
|
||||
@@ -3114,6 +3193,80 @@ export default function WorkOrdersPanel() {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={`客户退款 · ${getDisplayOrderNo(customerRefundOrder)}`}
|
||||
open={Boolean(customerRefundOrder)}
|
||||
onCancel={() => {
|
||||
setCustomerRefundOrder(null)
|
||||
setCustomerRefundPreview(null)
|
||||
customerRefundForm.resetFields()
|
||||
}}
|
||||
onOk={() => customerRefundForm.submit()}
|
||||
okText="同意退款并关闭订单"
|
||||
okButtonProps={{
|
||||
danger: true,
|
||||
disabled: customerRefundLoading || !customerRefundPreview?.canExecute,
|
||||
}}
|
||||
confirmLoading={customerRefundSubmitting}
|
||||
destroyOnHidden
|
||||
width={620}
|
||||
>
|
||||
{customerRefundLoading ? (
|
||||
<Alert type="info" showIcon message="正在查询平台用户售后单..." />
|
||||
) : customerRefundPreview ? (
|
||||
<Space direction="vertical" size={14} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="该操作会同意平台用户退款,并关闭订单下全部工单、资料提交、领取链接和待核销任务。"
|
||||
description="订单、退款、成本与资金流水会保留用于审计;存在已验收或已结算内容时系统会拒绝执行。"
|
||||
/>
|
||||
<Descriptions bordered size="small" column={1}>
|
||||
<Descriptions.Item label="平台">
|
||||
{customerRefundPreview.platform} / {customerRefundPreview.provider}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="平台订单号">
|
||||
{customerRefundPreview.platformOrderId}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="平台售后单号">
|
||||
{customerRefundPreview.externalAfterSaleId}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="退款金额">
|
||||
{formatMoney(customerRefundPreview.remainingFen)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="关闭范围">
|
||||
{customerRefundPreview.workOrderCount} 个工单 / {customerRefundPreview.itemCount}{' '}
|
||||
个订单项
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{customerRefundPreview.notes.length > 0 ? (
|
||||
<Alert
|
||||
type={customerRefundPreview.canExecute ? 'warning' : 'error'}
|
||||
showIcon
|
||||
message={customerRefundPreview.canExecute ? '处理提示' : '预检未通过'}
|
||||
description={customerRefundPreview.notes.join(';')}
|
||||
/>
|
||||
) : null}
|
||||
<Form
|
||||
form={customerRefundForm}
|
||||
layout="vertical"
|
||||
onFinish={(values) => void submitCustomerRefund(values)}
|
||||
>
|
||||
<Form.Item
|
||||
label="退款原因"
|
||||
name="reason"
|
||||
rules={[{ required: true, message: '请填写退款原因' }]}
|
||||
>
|
||||
<Input maxLength={200} />
|
||||
</Form.Item>
|
||||
<Form.Item label="处理备注" name="note">
|
||||
<Input.TextArea rows={3} maxLength={500} showCount />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Space>
|
||||
) : null}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={`撤单处理 · ${getDisplayOrderNo(cancelOrder)}`}
|
||||
open={Boolean(cancelOrder)}
|
||||
|
||||
@@ -67,7 +67,13 @@ const MESSAGE_TYPE_OPTIONS = [
|
||||
]
|
||||
|
||||
/** 默认本地处理白名单,作为 Select 的候选标签 */
|
||||
const DEFAULT_HANDLED_TOPICS = ['trade_TradePaid', 'trade_TradeCanceled', 'refund_RefundSuccess']
|
||||
const DEFAULT_HANDLED_TOPICS = [
|
||||
'trade_TradePaid',
|
||||
'trade_TradeCanceled',
|
||||
'refund_RefundCreated',
|
||||
'refund_RefundClosed',
|
||||
'refund_RefundSuccess',
|
||||
]
|
||||
|
||||
const DEFAULT_BASE_URL = 'https://dy-openplatform-api.wanmahui.com'
|
||||
|
||||
|
||||
@@ -54,6 +54,24 @@ type WorkOrderVoucherConsumeResult = {
|
||||
|
||||
export type AdminLeaderboardPeriod = 'all' | 'daily' | 'weekly' | 'monthly'
|
||||
|
||||
export type CustomerRefundPreview = {
|
||||
workOrderId: number
|
||||
orderId: number
|
||||
provider: string
|
||||
platform: string
|
||||
platformOrderId: string
|
||||
workOrderStatus: string
|
||||
remainingFen: number
|
||||
itemCount: number
|
||||
workOrderCount: number
|
||||
externalAfterSaleId: string
|
||||
platformUpdateTime: string
|
||||
platformAmountFen: number
|
||||
platformState: string
|
||||
canExecute: boolean
|
||||
notes: string[]
|
||||
}
|
||||
|
||||
export type AdminWorkerLeaderboardEntry = {
|
||||
rank: number
|
||||
workerId: number
|
||||
@@ -855,6 +873,29 @@ export function deleteAdminWorkOrder(workOrderId: number) {
|
||||
return apiDelete<{ deleted: boolean }>(`/api/v1/admin/worker-platform/orders/${workOrderId}`)
|
||||
}
|
||||
|
||||
export function previewAdminCustomerRefund(workOrderId: number) {
|
||||
return apiPost<CustomerRefundPreview>(
|
||||
`/api/v1/admin/worker-platform/orders/${workOrderId}/customer-refund/preview`,
|
||||
)
|
||||
}
|
||||
|
||||
export function executeAdminCustomerRefund(
|
||||
workOrderId: number,
|
||||
payload: { reason: string; note?: string },
|
||||
) {
|
||||
return apiPost<{
|
||||
idempotent: boolean
|
||||
operationId: number
|
||||
localRefundRecorded?: boolean
|
||||
workOrderId?: number
|
||||
orderId?: number
|
||||
cancelledWorkOrders?: number
|
||||
cancelledShares?: number
|
||||
releasedDepositFen?: number
|
||||
closedTaskCount?: number
|
||||
}>(`/api/v1/admin/worker-platform/orders/${workOrderId}/customer-refund`, payload)
|
||||
}
|
||||
|
||||
export function unpublishAdminWorkOrder(workOrderId: number) {
|
||||
return apiPost<{ order: WorkOrder }>(
|
||||
`/api/v1/admin/worker-platform/orders/${workOrderId}/unpublish`,
|
||||
|
||||
Reference in New Issue
Block a user