售后处置为补救完成或问题不成立时自动退还原追缴金额

This commit is contained in:
yml2213
2026-08-22 13:54:58 +08:00
parent 9beb501fa6
commit 6ac77c0124
12 changed files with 170 additions and 10 deletions
@@ -0,0 +1,7 @@
-- 售后处置为补救完成/问题不成立时,退还此前对同一责任单元已追缴的金额。
ALTER TABLE worker_after_sales_cases
ADD COLUMN IF NOT EXISTS refunded_amount INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS refunded_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS refunded_by TEXT NOT NULL DEFAULT '';
COMMENT ON COLUMN worker_after_sales_cases.refunded_amount IS '已退还的追缴金额;补救完成/问题不成立时按原追缴额全额退还';
@@ -271,6 +271,8 @@ export async function resolveAfterSalesCase(input: {
now: string
}): Promise<{
case: WorkerAfterSalesCaseRow | null
refundedRecoveries: Array<{ caseId: number; caseNo: string; amount: number }>
refundedTotalAmount: number
failureReason: 'not_found' | 'not_resolvable' | null
}> {
return withTransaction(async (client) => {
@@ -279,9 +281,20 @@ export async function resolveAfterSalesCase(input: {
[input.caseId],
)
const current = currentResult.rows[0] || null
if (!current) return { case: null, failureReason: 'not_found' }
if (!current)
return {
case: null,
refundedRecoveries: [],
refundedTotalAmount: 0,
failureReason: 'not_found',
}
if (!['awaiting_worker_response', 'under_review'].includes(current.status)) {
return { case: current, failureReason: 'not_resolvable' }
return {
case: current,
refundedRecoveries: [],
refundedTotalAmount: 0,
failureReason: 'not_resolvable',
}
}
let pendingDepositDeductionAmount = 0
@@ -350,7 +363,107 @@ export async function resolveAfterSalesCase(input: {
now: input.now,
})
}
return { case: updated, failureReason: null }
// 补救完成 / 问题不成立意味着责任解除:同一责任单元此前已追缴且未退还的金额应退还打手。
const refundedRecoveries: Array<{ caseId: number; caseNo: string; amount: number }> = []
if (updated && input.action !== 'upheld') {
const priorCases = await client.query<WorkerAfterSalesCaseRow>(
`
SELECT * FROM worker_after_sales_cases
WHERE work_order_id = $1
AND worker_id = $2
AND COALESCE(work_order_share_id, 0) = COALESCE($3, 0)
AND id != $4
AND resolution_action = 'upheld'
AND recovery_amount > 0
AND refunded_amount = 0
ORDER BY id ASC
FOR UPDATE
`,
[
Number(updated.work_order_id),
Number(updated.worker_id),
updated.work_order_share_id ? Number(updated.work_order_share_id) : null,
Number(updated.id),
],
)
for (const priorCase of priorCases.rows) {
const refundAmount = Number(priorCase.recovery_amount || 0)
if (refundAmount <= 0) continue
await client.query(
`
UPDATE worker_after_sales_debts
SET outstanding_amount = 0, status = 'settled', settled_at = $1, updated_at = $1
WHERE case_id = $2 AND status = 'open'
`,
[input.now, Number(priorCase.id)],
)
await ensureWorkerWalletWithClient(client, Number(priorCase.worker_id), input.now)
const wallet = await getWorkerWalletWithClient(client, Number(priorCase.worker_id))
const nextAvailable = Number(wallet?.available_amount || 0) + refundAmount
await client.query(
`UPDATE worker_wallets SET available_amount = $1, updated_at = $2 WHERE worker_id = $3`,
[nextAvailable, input.now, Number(priorCase.worker_id)],
)
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_refund', $2, $3, $4, $5, '售后追缴退还', $6::jsonb, $7)
`,
[
Number(priorCase.worker_id),
refundAmount,
nextAvailable,
Number(wallet?.frozen_deposit_amount || 0),
Number(priorCase.work_order_id),
JSON.stringify({
caseId: Number(priorCase.id),
triggerCaseId: Number(updated.id),
triggerAction: input.action,
}),
input.now,
],
)
await client.query(
`
UPDATE worker_after_sales_cases
SET refunded_amount = $1,
refunded_at = $2,
refunded_by = $3,
debt_amount = 0,
status = CASE WHEN status = 'recovery_pending' THEN 'closed' ELSE status END,
updated_at = $2
WHERE id = $4
`,
[refundAmount, input.now, input.resolvedBy, Number(priorCase.id)],
)
await createAfterSalesCaseEventWithClient(client, {
caseId: Number(priorCase.id),
actorType: 'admin',
actorId: input.resolvedBy,
eventType: 'recovery_refunded',
payloadJson: JSON.stringify({
amount: refundAmount,
triggerCaseId: Number(updated.id),
triggerAction: input.action,
}),
now: input.now,
})
refundedRecoveries.push({
caseId: Number(priorCase.id),
caseNo: String(priorCase.case_no || ''),
amount: refundAmount,
})
}
}
return {
case: updated,
refundedRecoveries,
refundedTotalAmount: refundedRecoveries.reduce((total, item) => total + item.amount, 0),
failureReason: null,
}
})
}
@@ -159,6 +159,9 @@ export type WorkerAfterSalesCaseRow = {
pending_deposit_deduction_amount: number
available_deduction_amount: number
debt_amount: number
refunded_amount?: number
refunded_at?: string | null
refunded_by?: string
created_by: string
resolved_by: string
resolved_at: string | null
@@ -212,6 +212,8 @@ export async function resolveAdminAfterSalesCase(
action,
recoveryAmount: Number(result.case.recovery_amount || 0),
debtAmount: Number(result.case.debt_amount || 0),
refundedTotalAmount: result.refundedTotalAmount,
refundedRecoveries: result.refundedRecoveries,
}),
now: nowIso(),
})
@@ -221,7 +223,11 @@ export async function resolveAdminAfterSalesCase(
workerIds: workerId > 0 ? [workerId] : [],
})
if (workerId > 0) publishWorkerWalletRealtimeChange(workerId)
return { case: mapWorkerAfterSalesCase(result.case) }
return {
case: mapWorkerAfterSalesCase(result.case),
refundedTotalAmount: result.refundedTotalAmount,
refundedRecoveries: result.refundedRecoveries,
}
}
export async function getWorkerAfterSalesCaseSummary(session: WorkerSession) {
@@ -198,6 +198,9 @@ export function mapWorkerAfterSalesCase(afterSalesCase: {
pending_deposit_deduction_amount: number
available_deduction_amount: number
debt_amount: number
refunded_amount?: number
refunded_at?: string | null
refunded_by?: string
created_by: string
resolved_by: string
resolved_at: string | null
@@ -245,6 +248,9 @@ export function mapWorkerAfterSalesCase(afterSalesCase: {
pendingDepositDeductionAmount: Number(afterSalesCase.pending_deposit_deduction_amount || 0),
availableDeductionAmount: Number(afterSalesCase.available_deduction_amount || 0),
debtAmount: Number(afterSalesCase.debt_amount || 0),
refundedAmount: Number(afterSalesCase.refunded_amount || 0),
refundedAt: afterSalesCase.refunded_at || null,
refundedBy: String(afterSalesCase.refunded_by || ''),
createdBy: String(afterSalesCase.created_by || ''),
resolvedBy: String(afterSalesCase.resolved_by || ''),
resolvedAt: afterSalesCase.resolved_at,
@@ -79,6 +79,7 @@ export function normalizeWalletLedgerType(value: unknown) {
'after_sales_deposit_deduction',
'after_sales_balance_deduction',
'after_sales_debt_offset',
'after_sales_refund',
].includes(ledgerType)
) {
return ledgerType
@@ -137,7 +137,10 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
resolutionNote: String(values.resolutionNote || '').trim(),
recoveryAmount: action === 'upheld' ? Number(values.recoveryAmount || 0) : 0,
}),
'售后问题单已处置',
(result) =>
(result.data?.refundedTotalAmount || 0) > 0
? `售后问题单已处置,已退还原追缴 ${formatMoney(result.data.refundedTotalAmount)} 至打手可用余额`
: '售后问题单已处置',
)
if (!succeeded) return
setAfterSalesCase(null)
@@ -264,6 +267,11 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
{formatMoney(item.recoveryAmount)} {formatMoney(item.debtAmount)}
</Typography.Text>
) : null}
{item.refundedAmount > 0 ? (
<Typography.Text type="success">
退 {formatMoney(item.refundedAmount)}
</Typography.Text>
) : null}
</div>
),
},
@@ -399,7 +407,11 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
>
<InputNumber min={0} precision={2} addonAfter="元" className="full-width" />
</Form.Item>
) : null
) : (
<Typography.Paragraph type="secondary" style={{ marginBottom: 16 }}>
退退
</Typography.Paragraph>
)
}
</Form.Item>
<Form.Item
@@ -352,6 +352,11 @@ function WorkerAfterSalesItem({
{formatMoney(item.debtAmount)}
</Typography.Text>
) : null}
{Number(item.refundedAmount || 0) > 0 ? (
<Typography.Text type="success" strong>
退 {formatMoney(item.refundedAmount)}
</Typography.Text>
) : null}
</Space>
) : null}
</Space>
@@ -37,6 +37,7 @@ const LEDGER_TYPE_OPTIONS = [
{ value: 'after_sales_deposit_deduction', label: '售后扣减押金' },
{ value: 'after_sales_balance_deduction', label: '售后扣减余额' },
{ value: 'after_sales_debt_offset', label: '抵扣售后欠款' },
{ value: 'after_sales_refund', label: '售后追缴退还' },
]
const REQUEST_TYPE_OPTIONS = [
@@ -60,6 +60,7 @@ export function formatLedgerType(ledgerType: string) {
after_sales_deposit_deduction: '售后扣减押金',
after_sales_balance_deduction: '售后扣减余额',
after_sales_debt_offset: '抵扣售后欠款',
after_sales_refund: '售后追缴退还',
}
return labels[ledgerType] || ledgerType || '-'
}
@@ -77,6 +78,7 @@ export function resolveLedgerTagColor(ledgerType: string) {
if (ledgerType === 'after_sales_deposit_deduction') return 'red'
if (ledgerType === 'after_sales_balance_deduction') return 'red'
if (ledgerType === 'after_sales_debt_offset') return 'orange'
if (ledgerType === 'after_sales_refund') return 'green'
return 'default'
}
@@ -509,10 +509,11 @@ export function resolveAdminAfterSalesCase(
recoveryAmount?: number
},
) {
return apiPost<{ case: WorkerAfterSalesCase }>(
`/api/v1/admin/worker-platform/after-sales-cases/${caseId}/resolve`,
payload,
)
return apiPost<{
case: WorkerAfterSalesCase
refundedTotalAmount: number
refundedRecoveries: Array<{ caseId: number; caseNo: string; amount: number }>
}>(`/api/v1/admin/worker-platform/after-sales-cases/${caseId}/resolve`, payload)
}
export function fetchAdminWorkOrderEvents(workOrderId: number) {
@@ -182,6 +182,9 @@ export type WorkerAfterSalesCase = {
pendingDepositDeductionAmount: number
availableDeductionAmount: number
debtAmount: number
refundedAmount: number
refundedAt: string | null
refundedBy: string
createdBy: string
resolvedBy: string
resolvedAt: string | null