diff --git a/apps/backend/src/db/migrations/056_worker_after_sales_refund.sql b/apps/backend/src/db/migrations/056_worker_after_sales_refund.sql new file mode 100644 index 00000000..a8ee1eca --- /dev/null +++ b/apps/backend/src/db/migrations/056_worker_after_sales_refund.sql @@ -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 '已退还的追缴金额;补救完成/问题不成立时按原追缴额全额退还'; diff --git a/apps/backend/src/repositories/worker-platform/after-sales-repo.ts b/apps/backend/src/repositories/worker-platform/after-sales-repo.ts index 21353711..e71cfdc6 100644 --- a/apps/backend/src/repositories/worker-platform/after-sales-repo.ts +++ b/apps/backend/src/repositories/worker-platform/after-sales-repo.ts @@ -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( + ` + 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, + } }) } diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index 83cf1219..6c26dec7 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -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 diff --git a/apps/backend/src/services/worker-platform/after-sales-service.ts b/apps/backend/src/services/worker-platform/after-sales-service.ts index db590a6f..6219a069 100644 --- a/apps/backend/src/services/worker-platform/after-sales-service.ts +++ b/apps/backend/src/services/worker-platform/after-sales-service.ts @@ -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) { diff --git a/apps/backend/src/services/worker-platform/worker-finance-mappers.ts b/apps/backend/src/services/worker-platform/worker-finance-mappers.ts index 9b1ef901..7dab816e 100644 --- a/apps/backend/src/services/worker-platform/worker-finance-mappers.ts +++ b/apps/backend/src/services/worker-platform/worker-finance-mappers.ts @@ -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, diff --git a/apps/backend/src/services/worker-platform/worker-platform-normalizers.ts b/apps/backend/src/services/worker-platform/worker-platform-normalizers.ts index cd8ece9c..12aaa838 100644 --- a/apps/backend/src/services/worker-platform/worker-platform-normalizers.ts +++ b/apps/backend/src/services/worker-platform/worker-platform-normalizers.ts @@ -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 diff --git a/apps/frontend/src/pages/admin/panels/AfterSalesPanel.tsx b/apps/frontend/src/pages/admin/panels/AfterSalesPanel.tsx index e2a92ff2..23540b9f 100644 --- a/apps/frontend/src/pages/admin/panels/AfterSalesPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/AfterSalesPanel.tsx @@ -137,7 +137,10 @@ const AfterSalesPanel = forwardRef(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(function AfterSalesPan 追缴 {formatMoney(item.recoveryAmount)},待还 {formatMoney(item.debtAmount)} ) : null} + {item.refundedAmount > 0 ? ( + + 已退还 {formatMoney(item.refundedAmount)} + + ) : null} ), }, @@ -399,7 +407,11 @@ const AfterSalesPanel = forwardRef(function AfterSalesPan > - ) : null + ) : ( + + 选择补救完成或问题不成立时,系统自动将该工单同一责任打手此前已追缴且未退还的金额退还至其可用余额。 + + ) } ) : null} + {Number(item.refundedAmount || 0) > 0 ? ( + + 已退还追缴 {formatMoney(item.refundedAmount)} 至可用余额 + + ) : null} ) : null} diff --git a/apps/frontend/src/pages/worker/WorkerProfileRecords.tsx b/apps/frontend/src/pages/worker/WorkerProfileRecords.tsx index a4d8b12b..2595124c 100644 --- a/apps/frontend/src/pages/worker/WorkerProfileRecords.tsx +++ b/apps/frontend/src/pages/worker/WorkerProfileRecords.tsx @@ -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 = [ diff --git a/apps/frontend/src/pages/worker/worker-profile-view-utils.tsx b/apps/frontend/src/pages/worker/worker-profile-view-utils.tsx index 652e6662..340d6ff6 100644 --- a/apps/frontend/src/pages/worker/worker-profile-view-utils.tsx +++ b/apps/frontend/src/pages/worker/worker-profile-view-utils.tsx @@ -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' } diff --git a/apps/frontend/src/services/admin/worker-platform.ts b/apps/frontend/src/services/admin/worker-platform.ts index 50cd5f4f..c8f63d34 100644 --- a/apps/frontend/src/services/admin/worker-platform.ts +++ b/apps/frontend/src/services/admin/worker-platform.ts @@ -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) { diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index 6ba2bbbd..de9ccad6 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -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