fix(worker-platform): 修复售后欠款抵扣导致验收失败,并支持余额偿还

有待还欠款时结算写入 settled_at 发生 timestamptz/text 类型错误会整单 500;顺带支持充值/解冻自动偿还与后台手动偿还。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-13 20:44:35 +08:00
co-authored by Cursor
parent aba508f4f6
commit a49db84771
10 changed files with 409 additions and 11 deletions
@@ -23,6 +23,7 @@ import {
createAdminAfterSalesCase,
fetchAdminAfterSalesCases,
fetchAdminWorkOrderSharing,
repayAdminAfterSalesCaseDebt,
resolveAdminAfterSalesCase,
} from '@/services/admin'
import type { UploadedFile, WorkerAfterSalesCase, WorkOrder } from '@/types/worker-platform'
@@ -43,6 +44,11 @@ type AfterSalesResolutionFormValues = {
recoveryAmount?: number
}
type AfterSalesRepayFormValues = {
amount?: number
note?: string
}
export type AfterSalesPanelHandle = {
openCreateModal: (row: WorkOrder) => void
}
@@ -52,11 +58,13 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
const queryClient = useQueryClient()
const [afterSalesOrder, setAfterSalesOrder] = useState<WorkOrder | null>(null)
const [afterSalesCase, setAfterSalesCase] = useState<WorkerAfterSalesCase | null>(null)
const [repayCase, setRepayCase] = useState<WorkerAfterSalesCase | null>(null)
const [afterSalesFiles, setAfterSalesFiles] = useState<UploadedFile[]>([])
const [afterSalesPage, setAfterSalesPage] = useState(1)
const [afterSalesPageSize, setAfterSalesPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
const [afterSalesCreateForm] = Form.useForm<AfterSalesCreateFormValues>()
const [afterSalesResolutionForm] = Form.useForm<AfterSalesResolutionFormValues>()
const [afterSalesRepayForm] = Form.useForm<AfterSalesRepayFormValues>()
const afterSalesSharingQuery = useQuery({
queryKey: ['admin-worker-platform-after-sales-sharing', afterSalesOrder?.workOrderId],
@@ -147,6 +155,31 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
afterSalesResolutionForm.resetFields()
}
function openAfterSalesRepayModal(item: WorkerAfterSalesCase) {
setRepayCase(item)
afterSalesRepayForm.setFieldsValue({
amount: Number(item.debtAmount || 0) / 100,
note: '后台手动偿还售后欠款',
})
}
async function submitAfterSalesRepay(values: AfterSalesRepayFormValues) {
if (!repayCase) return
const succeeded = await runAction(
() =>
repayAdminAfterSalesCaseDebt(repayCase.caseId, {
amount: Number(values.amount || 0),
note: String(values.note || '').trim(),
}),
(result) =>
`已偿还 ${formatMoney(result.data?.repaidAmount || 0)},余额剩余 ${formatMoney(result.data?.availableAfter || 0)}`,
)
if (!succeeded) return
setRepayCase(null)
afterSalesRepayForm.resetFields()
await queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-workers'] })
}
return (
<>
<Card
@@ -277,7 +310,7 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
},
{
title: '操作',
width: 120,
width: 160,
render: (_, item) =>
['awaiting_worker_response', 'under_review'].includes(item.status) ? (
<Button
@@ -287,6 +320,10 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
>
处置
</Button>
) : item.status === 'recovery_pending' && Number(item.debtAmount || 0) > 0 ? (
<Button type="primary" size="small" danger onClick={() => openAfterSalesRepayModal(item)}>
手动偿还
</Button>
) : (
<Typography.Text type="secondary">已完成</Typography.Text>
),
@@ -403,7 +440,7 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
label="追缴金额"
name="recoveryAmount"
rules={[{ required: true, message: '请输入追缴金额' }]}
extra="系统依次扣减该责任打手的本单待解冻金额、可用余额;不足部分转为欠款,并从后续验收报酬自动抵扣。"
extra="系统依次扣减该责任打手的本单待解冻金额、可用余额;不足部分转为欠款,可从后续验收报酬、充值入账自动抵扣,也可在此手动偿还。"
>
<InputNumber min={0} precision={2} addonAfter="元" className="full-width" />
</Form.Item>
@@ -424,6 +461,50 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
</Form>
) : null}
</Modal>
<Modal
title={`手动偿还欠款 · ${repayCase?.caseNo || ''}`}
open={Boolean(repayCase)}
onCancel={() => {
setRepayCase(null)
afterSalesRepayForm.resetFields()
}}
onOk={() => afterSalesRepayForm.submit()}
okText="确认偿还"
destroyOnHidden
>
{repayCase ? (
<Form form={afterSalesRepayForm} layout="vertical" onFinish={submitAfterSalesRepay}>
<Alert
showIcon
type="info"
style={{ marginBottom: 16 }}
message={`将从打手「${repayCase.workerName || '-'}」可用余额扣减,当前待还 ${formatMoney(repayCase.debtAmount)}。`}
/>
<Form.Item
label="偿还金额"
name="amount"
rules={[{ required: true, message: '请输入偿还金额' }]}
extra="默认偿还全部待还;也可填写部分金额。"
>
<InputNumber
min={0.01}
max={Number(repayCase.debtAmount || 0) / 100}
precision={2}
addonAfter="元"
className="full-width"
/>
</Form.Item>
<Form.Item
label="偿还备注"
name="note"
rules={[{ required: true, message: '请填写偿还备注' }]}
>
<Input.TextArea rows={3} />
</Form.Item>
</Form>
) : null}
</Modal>
</>
)
})
@@ -582,6 +582,20 @@ export function resolveAdminAfterSalesCase(
}>(`/api/v1/admin/worker-platform/after-sales-cases/${caseId}/resolve`, payload)
}
export function repayAdminAfterSalesCaseDebt(
caseId: number,
payload: {
amount?: number | null
note?: string
} = {},
) {
return apiPost<{
case: WorkerAfterSalesCase
repaidAmount: number
availableAfter: number
}>(`/api/v1/admin/worker-platform/after-sales-cases/${caseId}/repay`, payload)
}
export function fetchAdminWorkOrderEvents(workOrderId: number) {
return apiGet<{ items: WorkOrderEvent[] }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/events`,
+1
View File
@@ -53,6 +53,7 @@ export function formatAuditAction(action: string) {
work_order_pending_deposit_deducted: '押金扣除',
after_sales_case_created: '售后问题单创建',
after_sales_case_resolved: '售后问题单处置',
after_sales_case_debt_repaid: '售后欠款手动偿还',
worker_level_saved: '接单等级保存',
worker_level_deleted: '接单等级删除',
work_category_saved: '接单分类保存',
+1
View File
@@ -80,6 +80,7 @@ export const adminAuditActionOptions = [
{ label: '押金扣除', value: 'work_order_pending_deposit_deducted' },
{ label: '售后问题单创建', value: 'after_sales_case_created' },
{ label: '售后问题单处置', value: 'after_sales_case_resolved' },
{ label: '售后欠款手动偿还', value: 'after_sales_case_debt_repaid' },
{ label: '接单等级保存', value: 'worker_level_saved' },
{ label: '接单等级删除', value: 'worker_level_deleted' },
{ label: '接单分类保存', value: 'work_category_saved' },