fix(consignment): 售后立案回落到供货卖家,修内部现货单立不了案

补货款时排查"只认接单人"的地方,发现同类第三处:售后立案的责任人取
work_orders.assigned_worker_id,取不到就报 worker_missing(界面 409「请指定已验收的责任
打手或拼单份额」)。内部现货履约的寄卖单没有接单人,于是这类单子连售后都立不了案;
而货款修好后钱已结算给供货卖家,担责人本来就该是他。

- 抽出共享查询 getConsignmentSupplierByWorkOrderWithClient()
  (consignment-supplier-repo.ts):工单 material_json.consignment.inventoryId 或库存
  bound_work_order_id → 库存 → 提报单,货款结算与售后立案共用同一份口径
- 售后在找不到接单人时回落到供货卖家,并在验收快照里标记 consignmentSupplier
  与提报单号留痕
- 加测试钉住:售后必须先认寄卖关系再判定 worker_missing;取卖家的 SQL 只有一处实现

真实库验证(内部现货履约,接单人为空):反查供货卖家 workerId=3 正确;
售后立案成功且责任人 worker_id=3。临时数据残留 0。
This commit is contained in:
yml2213
2026-09-17 12:08:39 +08:00
parent 896194aa2b
commit f62f9b6645
5 changed files with 147 additions and 48 deletions
@@ -3,6 +3,7 @@ import type { PoolClient } from 'pg'
import { query, withTransaction } from '../../db/client.js'
import { resolveAfterSalesRecoveryAllocation } from './after-sales-recovery.js'
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
import { getConsignmentSupplierByWorkOrderWithClient } from './consignment-supplier-repo.js'
import type {
WorkerAfterSalesCaseEventRow,
WorkerAfterSalesCaseRow,
@@ -89,6 +90,19 @@ export async function createAfterSalesCase(input: {
shareQuantity: Number(share.quantity || 0),
}
}
if (workerId <= 0) {
// 内部现货履约的寄卖单没有接单打手,但货是供货卖家交付的、货款也结算给了他,
// 售后要担责的人就是他;不认寄卖关系的话这类单子连案都立不了。
const supplier = await getConsignmentSupplierByWorkOrderWithClient(client, input.workOrderId)
workerId = Number(supplier?.workerId || 0)
if (workerId > 0) {
acceptanceSnapshot = {
...acceptanceSnapshot,
consignmentSupplier: true,
consignmentSubmissionNo: supplier?.submissionNo || '',
}
}
}
if (workerId <= 0) {
return { case: null, failureReason: 'worker_missing' }
}
@@ -5,6 +5,7 @@ import { ensureWorkerWalletWithClient, getWorkerWalletWithClient, toJsonString }
import { offsetWorkerAfterSalesDebtsWithClient } from './after-sales-repo.js'
import { enqueueDepositUnfreezeWithClient } from './work-order-deposit-repo.js'
import { maybeUpgradeWorkerLevelWithClient } from './worker-ranking-repo.js'
import { getConsignmentSupplierByWorkOrderWithClient } from './consignment-supplier-repo.js'
/**
* 查询寄卖提报押金尚未释放的金额(分)
@@ -448,53 +449,11 @@ export async function settleConsignmentSupplierPayoutByWorkOrderWithClient(
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 supplier = await getConsignmentSupplierByWorkOrderWithClient(client, input.workOrderId)
if (!supplier) 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))
const supplierId = supplier.workerId
const amountFen = supplier.costPriceFen || supplier.askingPriceFen
if (supplierId <= 0 || amountFen <= 0) return null
// 接单人就是供货卖家时,货款已经随工单报酬结算过,不重复付款
if (Number(input.assignedWorkerId || 0) === supplierId) return null
@@ -556,8 +515,8 @@ export async function settleConsignmentSupplierPayoutByWorkOrderWithClient(
: `寄卖货款结算 #${input.workOrderId}`,
toJsonString({
workOrderId: Number(input.workOrderId),
inventoryId: Number(target.inventory_id),
submissionNo: target.submission_no || '',
inventoryId: supplier.inventoryId,
submissionNo: supplier.submissionNo,
costPriceFen: amountFen,
...(shouldDelayUnfreeze ? { unfreezeDays: input.unfreezeDays } : {}),
}),
@@ -0,0 +1,73 @@
import type { PoolClient } from 'pg'
/**
* 从工单反查寄卖的**供货卖家**(交付这件货、该收货款的人)。
*
* 为什么需要它:寄卖单在"内部现货履约"时是客服/内部仓代交付,工单上**没有接单打手**
* (`assigned_worker_id` 为空),于是所有"只认接单人"的逻辑都会落空——
* 货款没人收、售后找不到责任人都属于这一类。供货卖家只能从寄卖关系里查:
* 工单(`material_json.consignment.inventoryId` 或库存的 `bound_work_order_id`)
* → 库存 → 提报单。
*/
export type ConsignmentSupplier = {
workerId: number
inventoryId: number
costPriceFen: number
askingPriceFen: number
submissionNo: string
}
export async function getConsignmentSupplierByWorkOrderWithClient(
client: PoolClient,
workOrderId: number | string,
): Promise<ConsignmentSupplier | null> {
const res = await client.query<{
inventory_id: number
inventory_worker_id: number | null
cost_price_fen: string | number | null
submission_worker_id: number | null
asking_price_fen: string | number | null
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
`,
[workOrderId],
)
const row = res.rows[0]
if (!row) return null
return {
workerId: Number(row.inventory_worker_id || 0) || Number(row.submission_worker_id || 0),
inventoryId: Number(row.inventory_id),
costPriceFen: Math.round(Number(row.cost_price_fen || 0)),
askingPriceFen: Math.round(Number(row.asking_price_fen || 0)),
submissionNo: String(row.submission_no || ''),
}
}
@@ -43,3 +43,31 @@ test('寄卖货款:接单人就是供货卖家时不重复付款,金额取
assert.ok(source.includes('offsetWorkerAfterSalesDebtsWithClient('))
assert.ok(source.includes('enqueueDepositUnfreezeWithClient('))
})
test('售后立案:内部现货履约(无接单打手)时要落到供货卖家,否则连案都立不了', async () => {
const afterSalesSource = await readFile(
new URL('../../repositories/worker-platform/after-sales-repo.ts', import.meta.url),
'utf8',
)
assert.ok(afterSalesSource.includes('getConsignmentSupplierByWorkOrderWithClient('))
assert.ok(afterSalesSource.includes('worker_missing'))
assert.ok(
afterSalesSource.indexOf('getConsignmentSupplierByWorkOrderWithClient(') <
afterSalesSource.lastIndexOf("'worker_missing'"),
'必须先尝试认寄卖关系,再判定找不到责任人',
)
})
test('"从工单找供货卖家"只有一处实现,货款与售后共用', async () => {
const supplierRepo = await readFile(
new URL('../../repositories/worker-platform/consignment-supplier-repo.ts', import.meta.url),
'utf8',
)
assert.ok(
supplierRepo.includes('export async function getConsignmentSupplierByWorkOrderWithClient'),
)
const depositSource = await readFile(DEPOSIT_REPO, 'utf8')
assert.ok(depositSource.includes('getConsignmentSupplierByWorkOrderWithClient('))
// 取卖家那段 SQL(含 UNION ALL 兜底)只应存在于共享查询里,别处不要再抄一份
assert.equal(depositSource.includes('UNION ALL'), false)
})