feat(worker-platform): 支持客户退款冲正并关闭订单

This commit is contained in:
yml2213
2026-11-05 17:15:41 +08:00
parent b72d5877da
commit fc928c6b18
15 changed files with 2129 additions and 6 deletions
@@ -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`,