fix(worker-platform): 修复售后欠款抵扣导致验收失败,并支持余额偿还
有待还欠款时结算写入 settled_at 发生 timestamptz/text 类型错误会整单 500;顺带支持充值/解冻自动偿还与后台手动偿还。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -393,7 +393,8 @@ export async function resolveAfterSalesCase(input: {
|
||||
await client.query(
|
||||
`
|
||||
UPDATE worker_after_sales_debts
|
||||
SET outstanding_amount = 0, status = 'settled', settled_at = $1, updated_at = $1
|
||||
SET outstanding_amount = 0, status = 'settled',
|
||||
settled_at = $1::timestamptz, updated_at = $1::timestamptz
|
||||
WHERE case_id = $2 AND status = 'open'
|
||||
`,
|
||||
[input.now, Number(priorCase.id)],
|
||||
@@ -470,21 +471,38 @@ export async function resolveAfterSalesCase(input: {
|
||||
/** 在验收结算同一事务中抵扣未结清售后欠款,返回本次各售后单的抵扣明细。 */
|
||||
export async function offsetWorkerAfterSalesDebtsWithClient(
|
||||
client: PoolClient,
|
||||
input: { workerId: number; amount: number; workOrderId: number; now: string },
|
||||
input: {
|
||||
workerId: number
|
||||
amount: number
|
||||
workOrderId: number | null
|
||||
now: string
|
||||
caseId?: number
|
||||
actorType?: string
|
||||
actorId?: string
|
||||
},
|
||||
): Promise<Array<{ caseId: number; amount: number }>> {
|
||||
let remaining = Math.max(0, input.amount)
|
||||
if (remaining <= 0) return []
|
||||
const params: unknown[] = [input.workerId]
|
||||
let caseFilter = ''
|
||||
if (input.caseId && input.caseId > 0) {
|
||||
params.push(input.caseId)
|
||||
caseFilter = ` AND case_id = $${params.length}`
|
||||
}
|
||||
const debtsResult = await client.query<WorkerAfterSalesDebtRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM worker_after_sales_debts
|
||||
WHERE worker_id = $1 AND status = 'open' AND outstanding_amount > 0
|
||||
${caseFilter}
|
||||
ORDER BY created_at ASC, id ASC
|
||||
FOR UPDATE
|
||||
`,
|
||||
[input.workerId],
|
||||
params,
|
||||
)
|
||||
const offsets: Array<{ caseId: number; amount: number }> = []
|
||||
const actorType = String(input.actorType || 'system').trim() || 'system'
|
||||
const actorId = String(input.actorId || '').trim()
|
||||
for (const debt of debtsResult.rows) {
|
||||
if (remaining <= 0) break
|
||||
const outstanding = Number(debt.outstanding_amount || 0)
|
||||
@@ -495,8 +513,8 @@ export async function offsetWorkerAfterSalesDebtsWithClient(
|
||||
`
|
||||
UPDATE worker_after_sales_debts
|
||||
SET outstanding_amount = $1, status = $2,
|
||||
settled_at = CASE WHEN $2 = 'settled' THEN $3 ELSE NULL END,
|
||||
updated_at = $3
|
||||
settled_at = CASE WHEN $2 = 'settled' THEN $3::timestamptz ELSE NULL END,
|
||||
updated_at = $3::timestamptz
|
||||
WHERE id = $4
|
||||
`,
|
||||
[nextOutstanding, settled ? 'settled' : 'open', input.now, debt.id],
|
||||
@@ -506,17 +524,21 @@ export async function offsetWorkerAfterSalesDebtsWithClient(
|
||||
UPDATE worker_after_sales_cases
|
||||
SET debt_amount = $1,
|
||||
status = CASE WHEN $1 = 0 AND status = 'recovery_pending' THEN 'closed' ELSE status END,
|
||||
updated_at = $2
|
||||
updated_at = $2::timestamptz
|
||||
WHERE id = $3
|
||||
`,
|
||||
[nextOutstanding, input.now, debt.case_id],
|
||||
)
|
||||
await createAfterSalesCaseEventWithClient(client, {
|
||||
caseId: Number(debt.case_id),
|
||||
actorType: 'system',
|
||||
actorId: '',
|
||||
actorType,
|
||||
actorId,
|
||||
eventType: settled ? 'debt_settled' : 'debt_offset',
|
||||
payloadJson: JSON.stringify({ workOrderId: input.workOrderId, amount: offsetAmount }),
|
||||
payloadJson: JSON.stringify({
|
||||
workOrderId: input.workOrderId,
|
||||
amount: offsetAmount,
|
||||
source: actorType === 'admin' ? 'manual_repay' : 'settlement_offset',
|
||||
}),
|
||||
now: input.now,
|
||||
})
|
||||
offsets.push({ caseId: Number(debt.case_id), amount: offsetAmount })
|
||||
@@ -525,6 +547,159 @@ export async function offsetWorkerAfterSalesDebtsWithClient(
|
||||
return offsets
|
||||
}
|
||||
|
||||
/**
|
||||
* 从打手可用余额偿还售后欠款(充值入账或后台手动偿还)。
|
||||
* 不传 caseId 时按创建时间 FIFO 抵扣该打手全部 open 欠款。
|
||||
*/
|
||||
export async function repayWorkerAfterSalesDebtsFromAvailableWithClient(
|
||||
client: PoolClient,
|
||||
input: {
|
||||
workerId: number
|
||||
now: string
|
||||
caseId?: number
|
||||
maxAmount?: number
|
||||
workOrderId?: number | null
|
||||
note?: string
|
||||
actorType?: string
|
||||
actorId?: string
|
||||
},
|
||||
): Promise<{
|
||||
repaidTotal: number
|
||||
offsets: Array<{ caseId: number; amount: number }>
|
||||
availableAfter: number
|
||||
}> {
|
||||
await ensureWorkerWalletWithClient(client, input.workerId, input.now)
|
||||
const wallet = await getWorkerWalletWithClient(client, input.workerId)
|
||||
const availableAmount = Math.max(0, Number(wallet?.available_amount || 0))
|
||||
const budget = Math.min(
|
||||
availableAmount,
|
||||
input.maxAmount !== undefined && input.maxAmount !== null
|
||||
? Math.max(0, Number(input.maxAmount) || 0)
|
||||
: availableAmount,
|
||||
)
|
||||
if (budget <= 0) {
|
||||
return { repaidTotal: 0, offsets: [], availableAfter: availableAmount }
|
||||
}
|
||||
const offsets = await offsetWorkerAfterSalesDebtsWithClient(client, {
|
||||
workerId: input.workerId,
|
||||
amount: budget,
|
||||
workOrderId: input.workOrderId ?? null,
|
||||
now: input.now,
|
||||
...(input.caseId ? { caseId: input.caseId } : {}),
|
||||
...(input.actorType ? { actorType: input.actorType } : {}),
|
||||
...(input.actorId ? { actorId: input.actorId } : {}),
|
||||
})
|
||||
const repaidTotal = offsets.reduce((total, item) => total + item.amount, 0)
|
||||
if (repaidTotal <= 0) {
|
||||
return { repaidTotal: 0, offsets: [], availableAfter: availableAmount }
|
||||
}
|
||||
const nextAvailable = availableAmount - repaidTotal
|
||||
const nextFrozen = Number(wallet?.frozen_deposit_amount || 0)
|
||||
await client.query(
|
||||
`UPDATE worker_wallets SET available_amount = $1, updated_at = $2::timestamptz WHERE worker_id = $3`,
|
||||
[nextAvailable, input.now, input.workerId],
|
||||
)
|
||||
for (const debtOffset of offsets) {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO worker_wallet_ledgers (
|
||||
worker_id, ledger_type, amount, balance_after, frozen_after,
|
||||
related_work_order_id, note, payload_json, created_at
|
||||
) VALUES ($1, 'after_sales_debt_offset', $2, $3, $4, $5, $6, $7::jsonb, $8::timestamptz)
|
||||
`,
|
||||
[
|
||||
input.workerId,
|
||||
-debtOffset.amount,
|
||||
nextAvailable,
|
||||
nextFrozen,
|
||||
input.workOrderId ?? null,
|
||||
String(input.note || '可用余额偿还售后欠款').trim() || '可用余额偿还售后欠款',
|
||||
JSON.stringify({
|
||||
caseId: debtOffset.caseId,
|
||||
workOrderId: input.workOrderId ?? null,
|
||||
source: input.actorType === 'admin' ? 'manual_repay' : 'balance_repay',
|
||||
}),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
}
|
||||
return { repaidTotal, offsets, availableAfter: nextAvailable }
|
||||
}
|
||||
|
||||
/** 后台按售后单手动从可用余额偿还欠款。 */
|
||||
export async function repayAfterSalesCaseDebt(input: {
|
||||
caseId: number
|
||||
amount?: number | null
|
||||
note: string
|
||||
repaidBy: string
|
||||
now: string
|
||||
}): Promise<{
|
||||
case: WorkerAfterSalesCaseRow | null
|
||||
repaidAmount: number
|
||||
availableAfter: number
|
||||
failureReason: 'not_found' | 'no_open_debt' | 'insufficient_balance' | null
|
||||
}> {
|
||||
return withTransaction(async (client) => {
|
||||
const caseResult = await client.query<WorkerAfterSalesCaseRow>(
|
||||
`SELECT * FROM worker_after_sales_cases WHERE id = $1 FOR UPDATE`,
|
||||
[input.caseId],
|
||||
)
|
||||
const afterSalesCase = caseResult.rows[0] || null
|
||||
if (!afterSalesCase) {
|
||||
return { case: null, repaidAmount: 0, availableAfter: 0, failureReason: 'not_found' }
|
||||
}
|
||||
const debtAmount = Math.max(0, Number(afterSalesCase.debt_amount || 0))
|
||||
if (debtAmount <= 0 || afterSalesCase.status !== 'recovery_pending') {
|
||||
return {
|
||||
case: afterSalesCase,
|
||||
repaidAmount: 0,
|
||||
availableAfter: 0,
|
||||
failureReason: 'no_open_debt',
|
||||
}
|
||||
}
|
||||
const requestedAmount =
|
||||
input.amount !== undefined && input.amount !== null
|
||||
? Math.max(0, Number(input.amount) || 0)
|
||||
: debtAmount
|
||||
if (requestedAmount <= 0) {
|
||||
return {
|
||||
case: afterSalesCase,
|
||||
repaidAmount: 0,
|
||||
availableAfter: 0,
|
||||
failureReason: 'no_open_debt',
|
||||
}
|
||||
}
|
||||
const result = await repayWorkerAfterSalesDebtsFromAvailableWithClient(client, {
|
||||
workerId: Number(afterSalesCase.worker_id),
|
||||
now: input.now,
|
||||
caseId: Number(afterSalesCase.id),
|
||||
maxAmount: Math.min(requestedAmount, debtAmount),
|
||||
workOrderId: Number(afterSalesCase.work_order_id),
|
||||
note: input.note,
|
||||
actorType: 'admin',
|
||||
actorId: input.repaidBy,
|
||||
})
|
||||
if (result.repaidTotal <= 0) {
|
||||
return {
|
||||
case: afterSalesCase,
|
||||
repaidAmount: 0,
|
||||
availableAfter: result.availableAfter,
|
||||
failureReason: 'insufficient_balance',
|
||||
}
|
||||
}
|
||||
const refreshed = await client.query<WorkerAfterSalesCaseRow>(
|
||||
`${AFTER_SALES_CASE_SELECT} WHERE ac.id = $1`,
|
||||
[input.caseId],
|
||||
)
|
||||
return {
|
||||
case: refreshed.rows[0] || afterSalesCase,
|
||||
repaidAmount: result.repaidTotal,
|
||||
availableAfter: result.availableAfter,
|
||||
failureReason: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function recoverAfterSalesAmountWithClient(
|
||||
client: PoolClient,
|
||||
input: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { withTransaction } from '../../db/client.js'
|
||||
import { repayWorkerAfterSalesDebtsFromAvailableWithClient } from './after-sales-repo.js'
|
||||
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
|
||||
import type { WorkerDepositUnfreezeRow } from './types.js'
|
||||
import type { PoolClient } from 'pg'
|
||||
@@ -232,6 +233,14 @@ export async function releaseDepositUnfreeze({
|
||||
now,
|
||||
],
|
||||
)
|
||||
await repayWorkerAfterSalesDebtsFromAvailableWithClient(client, {
|
||||
workerId: Number(unfreeze.worker_id),
|
||||
now,
|
||||
workOrderId: Number(unfreeze.work_order_id),
|
||||
note: '解冻入账自动偿还售后欠款',
|
||||
actorType: 'system',
|
||||
actorId: 'deposit_unfreeze',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { query, withTransaction } from '../../db/client.js'
|
||||
import { repayWorkerAfterSalesDebtsFromAvailableWithClient } from './after-sales-repo.js'
|
||||
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
|
||||
import type {
|
||||
FinanceRequestListInput,
|
||||
@@ -485,6 +486,13 @@ export async function reviewWorkerFinanceRequest(input: {
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
await repayWorkerAfterSalesDebtsFromAvailableWithClient(client, {
|
||||
workerId: Number(current.worker_id),
|
||||
now: input.now,
|
||||
note: '充值审核入账自动偿还售后欠款',
|
||||
actorType: 'system',
|
||||
actorId: 'finance_recharge',
|
||||
})
|
||||
}
|
||||
} else if (
|
||||
input.status === 'rejected' &&
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { query, withTransaction } from '../../db/client.js'
|
||||
import { repayWorkerAfterSalesDebtsFromAvailableWithClient } from './after-sales-repo.js'
|
||||
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
|
||||
import type { WalletLedgerListInput, WorkerWalletLedgerRow, WorkerWalletRow } from './types.js'
|
||||
|
||||
@@ -78,6 +79,14 @@ export async function addWorkerWalletCredit(input: {
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
// 入账后自动偿还未结清售后欠款,避免余额够了却仍卡在追缴中。
|
||||
await repayWorkerAfterSalesDebtsFromAvailableWithClient(client, {
|
||||
workerId: input.workerId,
|
||||
now: input.now,
|
||||
note: '充值入账自动偿还售后欠款',
|
||||
actorType: 'system',
|
||||
actorId: 'wallet_credit',
|
||||
})
|
||||
return getWorkerWalletWithClient(client, input.workerId)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createAdminAfterSalesCase,
|
||||
getAdminAfterSalesCaseEvents,
|
||||
listAdminAfterSalesCases,
|
||||
repayAdminAfterSalesCaseDebt,
|
||||
resolveAdminAfterSalesCase,
|
||||
} from '../../../services/worker-platform/index.js'
|
||||
import { createJsonHandler, requireAdminRoles } from '../session.js'
|
||||
@@ -78,4 +79,28 @@ router.post(
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/after-sales-cases/:caseId/repay',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
repayAdminAfterSalesCaseDebt(
|
||||
String(req.params.caseId || ''),
|
||||
req.body || {},
|
||||
req.adminSession?.username || '',
|
||||
),
|
||||
{
|
||||
successMessage: '售后欠款已偿还',
|
||||
errorMessage: '偿还售后欠款失败',
|
||||
scope: '[admin/worker-platform/after-sales-cases/:caseId/repay]',
|
||||
audit: (req, data) => ({
|
||||
action: 'after_sales_case_debt_repaid',
|
||||
targetType: 'after_sales_case',
|
||||
targetId: String(req.params.caseId || ''),
|
||||
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
export default router
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
listAfterSalesCaseEvents,
|
||||
listAfterSalesCases,
|
||||
listWorkerAfterSalesCases,
|
||||
repayAfterSalesCaseDebt,
|
||||
resolveAfterSalesCase,
|
||||
submitWorkerAfterSalesResponse,
|
||||
} from '../../repositories/worker-platform/after-sales-repo.js'
|
||||
@@ -230,6 +231,80 @@ export async function resolveAdminAfterSalesCase(
|
||||
}
|
||||
}
|
||||
|
||||
export async function repayAdminAfterSalesCaseDebt(
|
||||
caseId: number | string,
|
||||
payload: JsonObject = {},
|
||||
actorName = '',
|
||||
) {
|
||||
const afterSalesCase = await getAfterSalesCaseById(caseId)
|
||||
if (!afterSalesCase) {
|
||||
throw createHttpError('售后问题单不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'after_sales_case_not_found',
|
||||
})
|
||||
}
|
||||
const note = String(payload.note || payload.resolutionNote || '后台手动偿还售后欠款').trim()
|
||||
if (!note) {
|
||||
throw createHttpError('请填写偿还备注', {
|
||||
statusCode: 400,
|
||||
errorCode: 'after_sales_case_repay_note_required',
|
||||
})
|
||||
}
|
||||
if (note.length > 2_000) {
|
||||
throw createHttpError('偿还备注不能超过 2000 个字符', {
|
||||
statusCode: 400,
|
||||
errorCode: 'after_sales_case_repay_note_too_long',
|
||||
})
|
||||
}
|
||||
const amountRaw = payload.amount ?? payload.repayAmount
|
||||
const amount =
|
||||
amountRaw === undefined || amountRaw === null || String(amountRaw).trim() === ''
|
||||
? null
|
||||
: normalizeAmountFen(amountRaw, 0)
|
||||
if (amount !== null && amount <= 0) {
|
||||
throw createHttpError('偿还金额必须大于 0', {
|
||||
statusCode: 400,
|
||||
errorCode: 'after_sales_case_repay_amount_invalid',
|
||||
})
|
||||
}
|
||||
const result = await repayAfterSalesCaseDebt({
|
||||
caseId: Number(caseId),
|
||||
amount,
|
||||
note,
|
||||
repaidBy: actorName,
|
||||
now: nowIso(),
|
||||
})
|
||||
if (result.failureReason === 'not_found' || !result.case) {
|
||||
throw createHttpError('售后问题单不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'after_sales_case_not_found',
|
||||
})
|
||||
}
|
||||
if (result.failureReason === 'no_open_debt') {
|
||||
throw createHttpError('该售后问题单没有待还欠款', {
|
||||
statusCode: 409,
|
||||
errorCode: 'after_sales_case_no_open_debt',
|
||||
})
|
||||
}
|
||||
if (result.failureReason === 'insufficient_balance') {
|
||||
throw createHttpError('打手可用余额不足,无法偿还欠款', {
|
||||
statusCode: 409,
|
||||
errorCode: 'after_sales_case_repay_insufficient_balance',
|
||||
})
|
||||
}
|
||||
const workerId = Number(result.case.worker_id)
|
||||
publishWorkOrderRealtimeChange({
|
||||
workOrderId: Number(result.case.work_order_id),
|
||||
workerIds: workerId > 0 ? [workerId] : [],
|
||||
})
|
||||
if (workerId > 0) publishWorkerWalletRealtimeChange(workerId)
|
||||
return {
|
||||
case: mapWorkerAfterSalesCase(result.case),
|
||||
repaidAmount: result.repaidAmount,
|
||||
availableAfter: result.availableAfter,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWorkerAfterSalesCaseSummary(session: WorkerSession) {
|
||||
requireActiveWorkerSession(session)
|
||||
return { items: (await listWorkerAfterSalesCases(session.workerId)).map(mapWorkerAfterSalesCase) }
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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: '接单分类保存',
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user