好友赠送单确认加好友后取消扣押金覆盖撤单撤回取消指派撤单申请与超时问题单处置

This commit is contained in:
yml2213
2026-08-23 11:23:17 +08:00
parent 70b31f4874
commit 360c5ea20e
6 changed files with 170 additions and 25 deletions
@@ -0,0 +1,60 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
isWorkOrderGiftDepositForfeitable,
normalizeWorkOrderAcceptanceMode,
resolveWorkOrderGiftPhase,
WORK_ORDER_ACCEPTANCE_MODE,
WORK_ORDER_GIFT_PHASE,
} from './work-order-acceptance-mode.js'
test('好友赠送押金不可退判定收敛确认加好友标记', () => {
assert.equal(
isWorkOrderGiftDepositForfeitable({
acceptance_mode: 'friend_gift',
friend_added_at: '2026-08-01T00:00:00.000Z',
}),
true,
)
// 未确认加好友(待交资料/已交资料阶段):押金仍可退
assert.equal(
isWorkOrderGiftDepositForfeitable({
acceptance_mode: 'friend_gift',
friend_added_at: null,
}),
false,
)
// 标准模式不受影响
assert.equal(
isWorkOrderGiftDepositForfeitable({
acceptance_mode: 'standard',
friend_added_at: '2026-08-01T00:00:00.000Z',
}),
false,
)
assert.equal(isWorkOrderGiftDepositForfeitable({}), false)
})
test('赠送阶段按冷却到点实时推导 gift_ready', () => {
const past = new Date(Date.now() - 60_000).toISOString()
const future = new Date(Date.now() + 60_000).toISOString()
assert.equal(
resolveWorkOrderGiftPhase({
acceptance_mode: WORK_ORDER_ACCEPTANCE_MODE.FRIEND_GIFT,
gift_phase: WORK_ORDER_GIFT_PHASE.FRIEND_COUNTDOWN,
gift_available_at: past,
}),
WORK_ORDER_GIFT_PHASE.GIFT_READY,
)
assert.equal(
resolveWorkOrderGiftPhase({
acceptance_mode: WORK_ORDER_ACCEPTANCE_MODE.FRIEND_GIFT,
gift_phase: WORK_ORDER_GIFT_PHASE.FRIEND_COUNTDOWN,
gift_available_at: future,
}),
WORK_ORDER_GIFT_PHASE.FRIEND_COUNTDOWN,
)
assert.equal(normalizeWorkOrderAcceptanceMode('friend_gift'), 'friend_gift')
assert.equal(normalizeWorkOrderAcceptanceMode(''), 'standard')
})
@@ -44,6 +44,7 @@ export type WorkOrderGiftSource = {
acceptance_mode?: string | null
gift_phase?: string | null
gift_available_at?: string | null
friend_added_at?: string | null
}
export function isWorkOrderGiftAvailable(source: WorkOrderGiftSource): boolean {
@@ -52,6 +53,11 @@ export function isWorkOrderGiftAvailable(source: WorkOrderGiftSource): boolean {
return Number.isFinite(availableAt) && availableAt <= Date.now()
}
/** 好友赠送单已由后台确认加好友后,取消/脱离打手不再退还押金,押金只能经验收结算。 */
export function isWorkOrderGiftDepositForfeitable(source: WorkOrderGiftSource): boolean {
return isFriendGiftAcceptanceMode(source.acceptance_mode) && Boolean(source.friend_added_at)
}
/** 返回当前赠送阶段;冷却到点后返回实时计算的 gift_ready。 */
export function resolveWorkOrderGiftPhase(source: WorkOrderGiftSource): string {
if (!isFriendGiftAcceptanceMode(source.acceptance_mode)) return ''
@@ -1,4 +1,5 @@
import { withTransaction } from '../../db/client.js'
import { isWorkOrderGiftDepositForfeitable } from '../../domain/work-order-acceptance-mode.js'
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
import { createWorkOrderEventWithClient } from './work-order-event-repo.js'
import { getWorkOrderByIdWithClient } from './work-order-query-repo.js'
@@ -124,6 +125,7 @@ export async function unassignWorkOrder(input: {
return { order: null, failureReason: 'work_order_no_worker' }
}
const forfeitGiftDeposit = isWorkOrderGiftDepositForfeitable(workOrder)
await ensureWorkerWalletWithClient(client, workerId, input.now)
const wallet = await getWorkerWalletWithClient(client, workerId)
const releaseAmount = Math.min(
@@ -147,7 +149,8 @@ export async function unassignWorkOrder(input: {
)
if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
const nextAvailable =
Number(wallet?.available_amount || 0) + (forfeitGiftDeposit ? 0 : releaseAmount)
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
await client.query(
`
@@ -162,9 +165,19 @@ export async function unassignWorkOrder(input: {
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, 'deposit_release', $2, $3, $4, $5, '后台取消指派退还押金', '{}'::jsonb, $6)
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
`,
[workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
[
workerId,
forfeitGiftDeposit ? 'deposit_deduction' : 'deposit_release',
forfeitGiftDeposit ? -releaseAmount : releaseAmount,
nextAvailable,
nextFrozen,
input.workOrderId,
forfeitGiftDeposit ? '后台取消指派扣除押金(已确认加好友)' : '后台取消指派退还押金',
JSON.stringify(forfeitGiftDeposit ? { giftDepositForfeited: true } : {}),
input.now,
],
)
}
@@ -175,7 +188,11 @@ export async function unassignWorkOrder(input: {
eventType: 'unassigned_by_admin',
fromStatus: 'in_progress',
toStatus: 'unassigned',
payloadJson: JSON.stringify({ workerId, releaseAmount }),
payloadJson: JSON.stringify({
workerId,
releaseAmount: forfeitGiftDeposit ? 0 : releaseAmount,
forfeitedAmount: forfeitGiftDeposit ? releaseAmount : 0,
}),
now: input.now,
})
@@ -214,6 +231,7 @@ export async function returnAssignedWorkOrderToHall(input: {
return { order: null, failureReason: 'work_order_no_worker' }
}
const forfeitGiftDeposit = isWorkOrderGiftDepositForfeitable(workOrder)
await ensureWorkerWalletWithClient(client, workerId, input.now)
const wallet = await getWorkerWalletWithClient(client, workerId)
const releaseAmount = Math.min(
@@ -239,7 +257,8 @@ export async function returnAssignedWorkOrderToHall(input: {
)
if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
const nextAvailable =
Number(wallet?.available_amount || 0) + (forfeitGiftDeposit ? 0 : releaseAmount)
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
await client.query(
`
@@ -254,9 +273,19 @@ export async function returnAssignedWorkOrderToHall(input: {
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, 'deposit_release', $2, $3, $4, $5, '后台退回大厅退还押金', '{}'::jsonb, $6)
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
`,
[workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
[
workerId,
forfeitGiftDeposit ? 'deposit_deduction' : 'deposit_release',
forfeitGiftDeposit ? -releaseAmount : releaseAmount,
nextAvailable,
nextFrozen,
input.workOrderId,
forfeitGiftDeposit ? '后台退回大厅扣除押金(已确认加好友)' : '后台退回大厅退还押金',
JSON.stringify(forfeitGiftDeposit ? { giftDepositForfeited: true } : {}),
input.now,
],
)
}
@@ -269,7 +298,8 @@ export async function returnAssignedWorkOrderToHall(input: {
toStatus: 'open',
payloadJson: JSON.stringify({
workerId,
releaseAmount,
releaseAmount: forfeitGiftDeposit ? 0 : releaseAmount,
forfeitedAmount: forfeitGiftDeposit ? releaseAmount : 0,
reason: String(input.reason || '').trim(),
}),
now: input.now,
@@ -310,6 +340,7 @@ export async function cancelAssignedWorkOrder(input: {
return { order: null, failureReason: 'work_order_no_worker' }
}
const forfeitGiftDeposit = isWorkOrderGiftDepositForfeitable(workOrder)
await ensureWorkerWalletWithClient(client, workerId, input.now)
const wallet = await getWorkerWalletWithClient(client, workerId)
const releaseAmount = Math.min(
@@ -326,6 +357,7 @@ export async function cancelAssignedWorkOrder(input: {
assigned_at = NULL,
deadline_at = NULL,
published_at = NULL,
${RESET_GIFT_FLOW_SQL},
updated_at = $1
WHERE id = $2
`,
@@ -333,7 +365,8 @@ export async function cancelAssignedWorkOrder(input: {
)
if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
const nextAvailable =
Number(wallet?.available_amount || 0) + (forfeitGiftDeposit ? 0 : releaseAmount)
const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
await client.query(
`
@@ -348,9 +381,19 @@ export async function cancelAssignedWorkOrder(input: {
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, 'deposit_release', $2, $3, $4, $5, '后台撤单退还押金', '{}'::jsonb, $6)
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
`,
[workerId, releaseAmount, nextAvailable, nextFrozen, input.workOrderId, input.now],
[
workerId,
forfeitGiftDeposit ? 'deposit_deduction' : 'deposit_release',
forfeitGiftDeposit ? -releaseAmount : releaseAmount,
nextAvailable,
nextFrozen,
input.workOrderId,
forfeitGiftDeposit ? '后台撤单扣除押金(已确认加好友)' : '后台撤单退还押金',
JSON.stringify(forfeitGiftDeposit ? { giftDepositForfeited: true } : {}),
input.now,
],
)
}
@@ -363,7 +406,8 @@ export async function cancelAssignedWorkOrder(input: {
toStatus: 'cancelled',
payloadJson: JSON.stringify({
workerId,
releaseAmount,
releaseAmount: forfeitGiftDeposit ? 0 : releaseAmount,
forfeitedAmount: forfeitGiftDeposit ? releaseAmount : 0,
reason: String(input.reason || '').trim(),
}),
now: input.now,
@@ -1,4 +1,5 @@
import { withTransaction } from '../../db/client.js'
import { isWorkOrderGiftDepositForfeitable } from '../../domain/work-order-acceptance-mode.js'
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
import { createWorkOrderEventWithClient } from './work-order-event-repo.js'
import { getWorkOrderByIdWithClient } from './work-order-query-repo.js'
@@ -57,8 +58,15 @@ export async function resolveProblemWorkOrder(input: {
restoreStatus = resolveProblemReturnToWorkerStatus(fromStatus)
}
const shouldReleaseDeposit = ['reopen', 'cancel_release'].includes(input.action)
const shouldDeductDeposit = input.action === 'cancel_deduct'
// 赠送单已确认加好友后不再退押金:释放类处置转为扣除,事件类型按实际资金动作记录。
const forfeitGiftDeposit = isWorkOrderGiftDepositForfeitable(workOrder)
const releaseActions = ['reopen', 'cancel_release']
const shouldReleaseDeposit = releaseActions.includes(input.action) && !forfeitGiftDeposit
const shouldDeductDeposit =
input.action === 'cancel_deduct' ||
(releaseActions.includes(input.action) && forfeitGiftDeposit)
const effectiveAction =
input.action === 'cancel_release' && forfeitGiftDeposit ? 'cancel_deduct' : input.action
const workerId = Number(workOrder.assigned_worker_id || 0)
let resolvedDepositAmount = 0
@@ -173,11 +181,12 @@ export async function resolveProblemWorkOrder(input: {
workOrderId: input.workOrderId,
actorType: 'admin',
actorId: input.actorName || '',
eventType: `problem_${input.action}`,
eventType: `problem_${effectiveAction}`,
fromStatus: 'problem',
toStatus: targetStatus,
payloadJson: JSON.stringify({
action: input.action,
giftDepositForfeited: forfeitGiftDeposit || undefined,
note: input.note,
depositAmount: resolvedDepositAmount,
}),
@@ -226,8 +235,15 @@ export async function settleOverdueWorkOrder(input: {
return { order: null, failureReason: 'work_order_has_active_shares' }
}
const shouldReleaseDeposit = ['reopen', 'cancel_release'].includes(input.policy)
const shouldDeductDeposit = input.policy === 'cancel_deduct'
// 赠送单已确认加好友后不再退押金:释放类处置转为扣除,事件类型按实际资金动作记录。
const forfeitGiftDeposit = isWorkOrderGiftDepositForfeitable(workOrder)
const releasePolicies = ['reopen', 'cancel_release']
const shouldReleaseDeposit = releasePolicies.includes(input.policy) && !forfeitGiftDeposit
const shouldDeductDeposit =
input.policy === 'cancel_deduct' ||
(releasePolicies.includes(input.policy) && forfeitGiftDeposit)
const effectivePolicy =
input.policy === 'cancel_release' && forfeitGiftDeposit ? 'cancel_deduct' : input.policy
const workerId = Number(workOrder.assigned_worker_id || 0)
let resolvedDepositAmount = 0
@@ -306,11 +322,12 @@ export async function settleOverdueWorkOrder(input: {
workOrderId: input.workOrderId,
actorType: 'system',
actorId: '',
eventType: `timeout_${input.policy}`,
eventType: `timeout_${effectivePolicy}`,
fromStatus: 'in_progress',
toStatus: targetStatus,
payloadJson: JSON.stringify({
policy: input.policy,
giftDepositForfeited: forfeitGiftDeposit || undefined,
depositAmount: resolvedDepositAmount,
workerId: workerId > 0 ? workerId : null,
}),
@@ -1,6 +1,7 @@
import type { PoolClient } from 'pg'
import { query, withTransaction } from '../../db/client.js'
import { isWorkOrderGiftDepositForfeitable } from '../../domain/work-order-acceptance-mode.js'
import {
ensureWorkerWalletWithClient,
getWorkerWalletWithClient,
@@ -458,6 +459,8 @@ export async function reviewWorkerCancelRequest(input: {
}
let releasedDepositAmount = 0
// 赠送单与拼单互斥,拼单份额分支不会命中赠送单;整单分支按确认加好友判定扣押金。
const forfeitGiftDeposit = isWorkOrderGiftDepositForfeitable(workOrder)
if (request.work_order_share_id) {
const shareResult = await client.query<WorkOrderShareRow>(
`SELECT * FROM work_order_shares WHERE id = $1 AND work_order_id = $2 FOR UPDATE`,
@@ -555,7 +558,8 @@ export async function reviewWorkerCancelRequest(input: {
[input.now, workOrder.id],
)
if (releasedDepositAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releasedDepositAmount
const nextAvailable =
Number(wallet?.available_amount || 0) + (forfeitGiftDeposit ? 0 : releasedDepositAmount)
const nextFrozen = Math.max(
0,
Number(wallet?.frozen_deposit_amount || 0) - releasedDepositAmount,
@@ -570,14 +574,22 @@ export async function reviewWorkerCancelRequest(input: {
`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, 'deposit_release', $2, $3, $4, $5, '撤单申请审核通过退还押金', $6::jsonb, $7)`,
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)`,
[
request.worker_id,
releasedDepositAmount,
forfeitGiftDeposit ? 'deposit_deduction' : 'deposit_release',
forfeitGiftDeposit ? -releasedDepositAmount : releasedDepositAmount,
nextAvailable,
nextFrozen,
request.work_order_id,
JSON.stringify({ requestId: request.id, reason: request.reason }),
forfeitGiftDeposit
? '撤单申请审核通过扣除押金(已确认加好友)'
: '撤单申请审核通过退还押金',
JSON.stringify({
requestId: request.id,
reason: request.reason,
giftDepositForfeited: forfeitGiftDeposit || undefined,
}),
input.now,
],
)
@@ -607,7 +619,8 @@ export async function reviewWorkerCancelRequest(input: {
payloadJson: JSON.stringify({
requestId: request.id,
shareId: request.work_order_share_id,
releasedDepositAmount,
releasedDepositAmount: forfeitGiftDeposit ? 0 : releasedDepositAmount,
forfeitedDepositAmount: forfeitGiftDeposit ? releasedDepositAmount : 0,
levelChanged: penalty.levelChanged,
accountDisabled: penalty.accountDisabled,
note: input.reviewNote,
@@ -617,7 +630,7 @@ export async function reviewWorkerCancelRequest(input: {
return {
request: await getWorkerCancelRequestByIdWithClient(client, request.id),
order: updatedOrder,
releasedDepositAmount,
releasedDepositAmount: forfeitGiftDeposit ? 0 : releasedDepositAmount,
levelChanged: penalty.levelChanged,
accountDisabled: penalty.accountDisabled,
failureReason: null,
@@ -107,10 +107,15 @@ export function formatWorkOrderEvent(event: WorkOrderEvent): EventNodeStyle {
return { title: '取消指派,订单回到未分配状态', color: 'green' }
case 'returned_to_hall_by_admin': {
const releaseAmount = readPayloadAmount(event, 'releaseAmount')
const forfeitedAmount = readPayloadAmount(event, 'forfeitedAmount')
return {
title: note ? `撤回订单到抢单大厅(${note}` : '撤回订单到抢单大厅',
description:
releaseAmount > 0 ? `已退回该打手冻结押金 ${formatFenAmount(releaseAmount)}` : undefined,
releaseAmount > 0
? `已退回该打手冻结押金 ${formatFenAmount(releaseAmount)}`
: forfeitedAmount > 0
? `已扣除该打手冻结押金 ${formatFenAmount(forfeitedAmount)}(好友赠送单已确认加好友,押金不退)`
: undefined,
color: 'green',
}
}