新增接单平台订单关闭与重新打开

This commit is contained in:
yml
2026-11-14 09:40:36 +08:00
parent 81a7721ee1
commit a0c7c90edf
19 changed files with 383 additions and 170 deletions
@@ -319,6 +319,14 @@ export function formatWorkOrderEvent(event: WorkOrderEvent): EventNodeStyle {
)}`,
color: 'red',
}
case 'closed_by_admin':
return {
title: note ? `关闭订单(${note})` : '关闭订单',
description: `订单状态:${formatStatusLabel(event.fromStatus)} → ${formatStatusLabel(
event.toStatus,
)}`,
color: 'red',
}
// —— 其他 ——
case 'payment_amount_updated': {
@@ -59,6 +59,7 @@ import {
cancelAdminWorkOrderShare,
cancelAdminWorkOrderSharing,
cancelAdminWorkOrderSharingPart,
closeAdminWorkOrder,
completeAdminWorkOrderGiftCooldown,
confirmAdminWorkOrderFriend,
deductAdminWorkOrderPendingDeposit,
@@ -72,11 +73,9 @@ import {
fetchAdminWorkerProductNameSuffix,
fetchAdminWorkerUsers,
fetchAdminWorkOrders,
executeAdminCustomerRefund,
markAdminWorkOrderProblem,
markAdminWorkOrderShareProblem,
pinAdminWorkOrder,
previewAdminCustomerRefund,
publishAdminWorkOrder,
reduceAdminWorkOrderShareQuantity,
reopenAdminCancelledWorkOrder,
@@ -89,7 +88,6 @@ import {
updateAdminWorkOrderNote,
updateAdminWorkOrderSharing,
} from '@/services/admin'
import type { CustomerRefundPreview } from '@/services/admin/worker-platform'
import {
checkWorkOrderConsignmentMatches,
matchAdminConsignmentWorkOrder,
@@ -205,11 +203,6 @@ 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)
@@ -311,9 +304,6 @@ 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')
@@ -389,12 +379,6 @@ 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>()
@@ -409,7 +393,6 @@ 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
@@ -709,44 +692,15 @@ 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 closeOrder(row: WorkOrder) {
modal.confirm({
title: '确认关闭该订单?',
content: '关闭后客户将无法访问订单服务,不会发起退款;订单仍保留在后台,可随时重新打开。',
okText: '关闭订单',
cancelText: '取消',
okButtonProps: { danger: true },
onOk: () => runAction(() => closeAdminWorkOrder(row.workOrderId), '订单已关闭'),
})
}
function applyMaterialPaste(text: string) {
@@ -2038,9 +1992,9 @@ export default function WorkOrdersPanel() {
icon={<UndoOutlined />}
onClick={() => {
modal.confirm({
title: '确认重新启用该订单?',
content: '订单将恢复为待分配状态。请确认资料与金额后,再发布到抢单大厅。',
okText: '重新启用',
title: '确认重新打开该订单?',
content: '订单将恢复为待分配状态,客户可重新访问并继续处理。',
okText: '重新打开',
cancelText: '取消',
onOk: () =>
runAction(
@@ -2050,7 +2004,7 @@ export default function WorkOrdersPanel() {
})
}}
>
重新启用
重新打开订单
</Button>
) : null}
{row.sharing?.enabled ||
@@ -2079,25 +2033,14 @@ export default function WorkOrdersPanel() {
内部库存匹配
</Button>
) : null}
{canCustomerRefund &&
[
'pending_material',
'unassigned',
'open',
'in_progress',
'pending_acceptance',
'problem',
].includes(String(row.rawStatus || row.status || '')) ? (
{row.capabilities?.canCloseOrder && canCancelOrder ? (
<Button
danger
icon={<RollbackOutlined />}
loading={
customerRefundLoading && customerRefundOrder?.workOrderId === row.workOrderId
}
onClick={() => void openCustomerRefundModal(row)}
title="同意平台用户退款,并关闭工单、资料、领取链接和待核销任务"
onClick={() => closeOrder(row)}
title="关闭订单后客户无法访问订单服务,可重新打开"
>
客户退款
关闭订单
</Button>
) : null}
{row.capabilities?.canCancel && (canReturnToHall || canCancelOrder) ? (
@@ -3226,80 +3169,6 @@ 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)}
@@ -602,6 +602,12 @@ export function cancelAdminWorkOrder(
)
}
export function closeAdminWorkOrder(workOrderId: number) {
return apiPost<{ order: WorkOrder; action: 'close' | 'cancel_order' }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/close`,
)
}
export function creditAdminWorkerWallet(
workerId: number,
payload: { amount: number; note?: string },
@@ -694,6 +694,7 @@ export type WorkOrder = {
canMarkProblem: boolean
canDelete: boolean
canReopen: boolean
canCloseOrder: boolean
canPublish: boolean
canCancel: boolean
canCancelSharing?: boolean
@@ -1,17 +0,0 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import test from 'node:test'
test('接单工单提供客户退款入口和确认弹窗', async () => {
const [panel, service] = await Promise.all([
readFile(new URL('../src/pages/admin/panels/WorkOrdersPanel.tsx', import.meta.url), 'utf8'),
readFile(new URL('../src/services/admin/worker-platform.ts', import.meta.url), 'utf8'),
])
assert.ok(panel.includes('客户退款'))
assert.ok(panel.includes('同意退款并关闭订单'))
assert.ok(panel.includes('worker_order.customer_refund'))
assert.ok(panel.includes('previewAdminCustomerRefund'))
assert.ok(panel.includes('executeAdminCustomerRefund'))
assert.ok(service.includes('/customer-refund/preview'))
assert.ok(service.includes('/customer-refund'))
})
@@ -0,0 +1,18 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import test from 'node:test'
test('接单工单隐藏客户退款并提供可逆关闭订单入口', async () => {
const [panel, service] = await Promise.all([
readFile(new URL('../src/pages/admin/panels/WorkOrdersPanel.tsx', import.meta.url), 'utf8'),
readFile(new URL('../src/services/admin/worker-platform.ts', import.meta.url), 'utf8'),
])
assert.equal(panel.includes('客户退款'), false)
assert.equal(panel.includes('previewAdminCustomerRefund'), false)
assert.equal(panel.includes('executeAdminCustomerRefund'), false)
assert.ok(panel.includes('关闭订单'))
assert.ok(panel.includes('关闭后客户将无法访问订单服务'))
assert.ok(panel.includes('重新打开订单'))
assert.ok(panel.includes('canCloseOrder'))
assert.ok(service.includes('/close'))
})