fix(consignment): 验收结算补上寄卖货款,并修指派打手线报酬 100 倍单位错

问题:工单 #100171 验收完成时卖家钱包只有押金 -20/+20,货卖掉了但货款收益没进卖家钱包。

查证:工单报酬 reward_amount=14500 分(发单方付 ¥145),货的结算价 cost_price_fen=12400 分
(卖家应得 ¥124),而 assigned_worker_id 为空(内部现货履约)。通用结算的规则是"把工单报酬
付给接单人",内部现货单没有接单人 → 这笔钱谁也没拿到,卖家只拿回自己的押金。

- 新增 settleConsignmentSupplierPayoutByWorkOrderWithClient():验收完成时把货的结算价
  (cost_price_fen,缺省退回 asking_price_fen)补给供货卖家,单位与通用报酬一致(分),
  同样做售后欠款抵扣、按售后策略决定是否延迟解冻、写 reward_settlement/reward_pending_unfreeze
  流水(说明「寄卖货款结算 #单号」)
- 不重复付款:接单人就是供货卖家本人时跳过(指派打手线里货物已随工单报酬付过)
- 修 100 倍单位错:指派打手线把工单报酬改写成货的结算价时写了 (cost_price_fen/100).toFixed(2),
  ¥124 被写成 ¥1.24;reward_amount 的单位是分(已用真实数据核对)。抽成纯函数
  resolveConsignmentAssignRewardFen() 并加测试钉住单位

真实库端到端验证:卖家钱包 ¥205.50 → ¥329.50(+¥124.00 寄卖货款结算),累计结算同步 +¥124.00;
验证脚本改动的开发库数据已还原,临时数据残留 0。
This commit is contained in:
yml2213
2026-09-17 12:04:39 +08:00
parent 40882aeebb
commit 896194aa2b
5 changed files with 289 additions and 6 deletions
@@ -2,6 +2,9 @@ import type { PoolClient } from 'pg'
import { withTransaction } from '../../db/client.js'
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient, toJsonString } from './shared.js'
import { offsetWorkerAfterSalesDebtsWithClient } from './after-sales-repo.js'
import { enqueueDepositUnfreezeWithClient } from './work-order-deposit-repo.js'
import { maybeUpgradeWorkerLevelWithClient } from './worker-ranking-repo.js'
/**
* 查询寄卖提报押金尚未释放的金额(分)
@@ -423,3 +426,165 @@ export async function forfeitConsignmentPublishDepositByWorkOrderWithClient(
now: input.now,
})
}
/**
* 结算寄卖**货款**:验收完成时把货的结算价(`cost_price_fen`)付给供货卖家。
*
* 背景:指派打手线里接单人就是供货卖家本人,工单报酬已经被改写为货的结算价、
* 由通用结算流程付给他,不能再付一次;而**内部现货履约**(客服代交付,订单没有接单打手)
* 时通用结算流程里没有这个人,货款就没人收 —— 卖家交付了货,却只拿回自己的押金。
*
* 所以这里只补"没人收"的那一份:接单人不是供货卖家(含没有接单人)时,按货的结算价补给卖家。
* 金额、售后抵扣与解冻策略都对齐通用报酬结算(`reward_amount` 的处理方式)。
*/
export async function settleConsignmentSupplierPayoutByWorkOrderWithClient(
client: PoolClient,
input: {
workOrderId: number | string
/** 通用结算已经付过的接单人;等于供货卖家时说明货款已随报酬付过 */
assignedWorkerId: number
rewardUnfreezeEnabled: boolean
unfreezeDays: number
now: string
},
): Promise<{ workerId: number; amountFen: number } | null> {
const res = await client.query<{
inventory_id: number
inventory_worker_id: number | null
cost_price_fen: number
submission_worker_id: number | null
asking_price_fen: number
submission_no: string | null
}>(
`
SELECT
inv.id AS inventory_id,
inv.worker_id AS inventory_worker_id,
inv.cost_price_fen,
cs.worker_id AS submission_worker_id,
cs.asking_price_fen,
cs.submission_no
FROM work_orders wo
JOIN consignment_inventories inv
ON inv.id = CASE
WHEN (wo.material_json->'consignment'->>'inventoryId') ~ '^[0-9]+$'
THEN (wo.material_json->'consignment'->>'inventoryId')::int
END
LEFT JOIN consignment_submissions cs ON cs.id = inv.source_submission_id
WHERE wo.id = $1
UNION ALL
SELECT
inv.id,
inv.worker_id,
inv.cost_price_fen,
cs.worker_id,
cs.asking_price_fen,
cs.submission_no
FROM consignment_inventories inv
LEFT JOIN consignment_submissions cs ON cs.id = inv.source_submission_id
WHERE inv.bound_work_order_id = $1
LIMIT 1
`,
[input.workOrderId],
)
const target = res.rows[0]
if (!target) return null
const supplierId =
Number(target.inventory_worker_id || 0) || Number(target.submission_worker_id || 0)
const amountFen =
Math.round(Number(target.cost_price_fen || 0)) ||
Math.round(Number(target.asking_price_fen || 0))
if (supplierId <= 0 || amountFen <= 0) return null
// 接单人就是供货卖家时,货款已经随工单报酬结算过,不重复付款
if (Number(input.assignedWorkerId || 0) === supplierId) return null
await ensureWorkerWalletWithClient(client, supplierId, input.now)
const wallet = await getWorkerWalletWithClient(client, supplierId)
const debtOffsets = await offsetWorkerAfterSalesDebtsWithClient(client, {
workerId: supplierId,
amount: amountFen,
workOrderId: Number(input.workOrderId),
now: input.now,
})
const debtOffsetAmount = debtOffsets.reduce((total, item) => total + item.amount, 0)
const shouldDelayUnfreeze = input.rewardUnfreezeEnabled && input.unfreezeDays > 0
const nextAvailable =
Number(wallet?.available_amount || 0) + (shouldDelayUnfreeze ? 0 : amountFen) - debtOffsetAmount
await client.query(
`
UPDATE worker_wallets
SET
available_amount = $1,
pending_unfreeze_amount = pending_unfreeze_amount + $2,
total_settled_amount = total_settled_amount + $3,
updated_at = $4
WHERE worker_id = $5
`,
[nextAvailable, shouldDelayUnfreeze ? amountFen : 0, amountFen, input.now, supplierId],
)
if (shouldDelayUnfreeze) {
await enqueueDepositUnfreezeWithClient(client, {
workerId: supplierId,
workOrderId: Number(input.workOrderId),
amount: amountFen,
fundType: 'reward',
unfreezeDays: input.unfreezeDays,
now: input.now,
})
}
const frozenAfter = Number(wallet?.frozen_deposit_amount || 0)
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, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
`,
[
supplierId,
shouldDelayUnfreeze ? 'reward_pending_unfreeze' : 'reward_settlement',
amountFen,
nextAvailable,
frozenAfter,
Number(input.workOrderId),
shouldDelayUnfreeze
? `寄卖货款待解冻 #${input.workOrderId}`
: `寄卖货款结算 #${input.workOrderId}`,
toJsonString({
workOrderId: Number(input.workOrderId),
inventoryId: Number(target.inventory_id),
submissionNo: target.submission_no || '',
costPriceFen: amountFen,
...(shouldDelayUnfreeze ? { unfreezeDays: input.unfreezeDays } : {}),
}),
input.now,
],
)
for (const debtOffset of debtOffsets) {
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::jsonb, $7)
`,
[
supplierId,
-debtOffset.amount,
nextAvailable,
frozenAfter,
Number(input.workOrderId),
toJsonString({ caseId: debtOffset.caseId, workOrderId: Number(input.workOrderId) }),
input.now,
],
)
}
await maybeUpgradeWorkerLevelWithClient(client, supplierId, input.now)
return { workerId: supplierId, amountFen }
}
@@ -15,7 +15,10 @@ import {
} from './work-order-deposit-repo.js'
import { maybeUpgradeWorkerLevelWithClient } from './worker-ranking-repo.js'
import type { WorkOrderRow, WorkOrderShareRow } from './types.js'
import { releaseConsignmentPublishDepositBySubmissionWithClient } from './consignment-deposit-repo.js'
import {
releaseConsignmentPublishDepositBySubmissionWithClient,
settleConsignmentSupplierPayoutByWorkOrderWithClient,
} from './consignment-deposit-repo.js'
export async function acceptWorkOrderAndSettle(input: {
workOrderId: number
@@ -421,6 +424,16 @@ export async function acceptWorkOrderAndSettle(input: {
}
}
// 寄卖货款:内部现货履约时订单没有接单打手,通用报酬结算里没人收这笔钱,
// 必须在这里补给供货卖家(接单人就是卖家本人时不重复付款,见函数注释)。
await settleConsignmentSupplierPayoutByWorkOrderWithClient(client, {
workOrderId: input.workOrderId,
assignedWorkerId: workerId,
rewardUnfreezeEnabled: input.rewardUnfreezeEnabled,
unfreezeDays: input.unfreezeDays,
now: input.now,
})
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: input.actorType || 'admin',
@@ -361,6 +361,21 @@ export async function fulfillWorkOrderWithConsignment(input: {
/**
* 选用内部寄售库存并走接单平台指派打手(冻结押金担保履约)
*/
/**
* 指派打手线里工单报酬要改成"货的结算价":卖家拿结算价、平台赚差价。
*
* 单位必须是**分**(与钱包、流水、其它工单报酬一致)。这里以前多除了 100,
* 会把 ¥124 写成 ¥1.24 —— 卖家只拿到结算价的百分之一。
*/
export function resolveConsignmentAssignRewardFen(input: {
costPriceFen: unknown
orderRewardFen: unknown
}): number {
const costPriceFen = Math.round(Number(input.costPriceFen || 0))
if (costPriceFen > 0) return costPriceFen
return Math.round(Number(input.orderRewardFen || 0))
}
export async function assignWorkOrderWithConsignmentWorker(input: {
workOrderId: number | string
inventoryId: number | string
@@ -460,14 +475,14 @@ export async function assignWorkOrderWithConsignmentWorker(input: {
},
}
const rewardAmountYuan =
Number(claimed.cost_price_fen || 0) > 0
? Number((Number(claimed.cost_price_fen) / 100).toFixed(2))
: Number(workOrder.reward_amount || 0)
const rewardAmountFen = resolveConsignmentAssignRewardFen({
costPriceFen: claimed.cost_price_fen,
orderRewardFen: workOrder.reward_amount,
})
await updateWorkOrder(workOrder.id, {
material_json: updatedMaterial,
reward_amount: rewardAmountYuan,
reward_amount: rewardAmountFen,
updated_at: now,
})
@@ -0,0 +1,45 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { readFile } from 'node:fs/promises'
import { resolveConsignmentAssignRewardFen } from './consignment-fulfillment-service.js'
const SETTLEMENT_REPO = new URL(
'../../repositories/worker-platform/work-order-settlement-repo.ts',
import.meta.url,
)
const DEPOSIT_REPO = new URL(
'../../repositories/worker-platform/consignment-deposit-repo.ts',
import.meta.url,
)
test('指派打手线的工单报酬用分(不再多除 100)', () => {
// ¥124.00 的货 → 12400 分。以前是 Number((12400 / 100).toFixed(2)) = 124,只等于 ¥1.24
assert.equal(
resolveConsignmentAssignRewardFen({ costPriceFen: 12400, orderRewardFen: 14500 }),
12400,
)
assert.equal(resolveConsignmentAssignRewardFen({ costPriceFen: 2500, orderRewardFen: 999 }), 2500)
})
test('没有结算价时退回原工单报酬,且都是分', () => {
assert.equal(resolveConsignmentAssignRewardFen({ costPriceFen: 0, orderRewardFen: 14500 }), 14500)
assert.equal(resolveConsignmentAssignRewardFen({ costPriceFen: null, orderRewardFen: null }), 0)
})
test('验收结算必须补寄卖货款,否则内部现货单的卖家只拿回押金', async () => {
const source = await readFile(SETTLEMENT_REPO, 'utf8')
assert.ok(source.includes('settleConsignmentSupplierPayoutByWorkOrderWithClient('))
assert.ok(source.includes('assignedWorkerId: workerId'))
})
test('寄卖货款:接单人就是供货卖家时不重复付款,金额取货的结算价', async () => {
const source = await readFile(DEPOSIT_REPO, 'utf8')
assert.ok(source.includes('const supplierId ='))
assert.ok(source.includes('if (Number(input.assignedWorkerId || 0) === supplierId) return null'))
assert.ok(source.includes('cost_price_fen'))
assert.ok(source.includes('寄卖货款结算'), '货款流水要有可读说明')
// 和通用报酬结算一样:售后欠款抵扣 + 按售后策略决定是否延迟解冻
assert.ok(source.includes('offsetWorkerAfterSalesDebtsWithClient('))
assert.ok(source.includes('enqueueDepositUnfreezeWithClient('))
})