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:
@@ -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)
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
| `632dddc4` | 上一轮:H5 渠道整体下线 + 凭据字段修复 + 状态机封闭(撤销守卫 / 库存绑定唯一性 / 生命周期解绑)+ 押金归属取台账 | 迁移 `079`、`consignment-*` 五个文件、`work-order-*` 三个仓库、两个前端面板 |
|
||||
| `5d11bf20` | 批次 C+D:指派部分成功补偿、凭证核销如实反馈、空规格不放行 + 履约/指派服务端规格复检 | `consignment-fulfillment-service.ts`、`consignment-matching-service.ts`、`consignment-repo.ts`、履约路由、`WorkOrdersPanel.tsx` |
|
||||
| `d6f6219f` | 批次 F1:内部现货履约停在「待加好友」,客服确认后才进赠送冷却 + 备注快捷项 | `work-order-gift-repo.ts`、履约服务、`WorkOrdersPanel.tsx`、`utils/gift-phase.ts` |
|
||||
| 本批提交(批次 R,同一提交内含本文档,故不写自身 hash) | 批次 R:售后立案回落到供货卖家;抽共享"从工单找供货卖家"查询 | `consignment-supplier-repo.ts`、`after-sales-repo.ts`、`consignment-deposit-repo.ts` |
|
||||
| 本批提交(批次 Q,同一提交内含本文档,故不写自身 hash) | 批次 Q:验收结算补寄卖货款给供货卖家;修指派打手线报酬 100 倍单位错 | `consignment-deposit-repo.ts`、`work-order-settlement-repo.ts`、`consignment-fulfillment-service.ts` |
|
||||
| 本批提交(批次 P,同一提交内含本文档,故不写自身 hash) | 批次 P:确认加好友/上传赠送截图恢复为卖家操作,客服兜底 | `consignment-item-stage.ts`、`consignment-worker-service.ts`、`worker-acceptance-service.ts` |
|
||||
| 本批提交(批次 O,同一提交内含本文档,故不写自身 hash) | 批次 O:文件链接去掉签名与有效期(修客服端凭证裂图),寄卖出参统一归一化 | `file-storage-service.ts`、`routes/files.ts`、`consignment-admin-service.ts` |
|
||||
@@ -287,6 +288,30 @@
|
||||
前端在"部分匹配"候选上先 `modal.confirm` 危险确认,再带 `allowPartial: true` 重试,高配充低配的合法玩法保留但不可误触。
|
||||
3. ⏳ 匹配下推 SQL + 批量匹配接口 + 工单列表角标(P2-8)未做:当前仍是"取全量在库 → 内存比对"。
|
||||
|
||||
### 批次 R · 售后立案也认寄卖关系(本轮)
|
||||
|
||||
补货款时顺手排查"只认接单人"的地方,发现同类第三处:**售后立案**。
|
||||
|
||||
`createAfterSalesCase` 里责任人取 `work_orders.assigned_worker_id`,取不到就
|
||||
`failureReason: 'worker_missing'` → 客服界面上是 409「请指定已验收的责任打手或拼单份额」。
|
||||
内部现货履约的寄卖单**没有接单人**,于是这类单子**连售后都立不了案**;
|
||||
而货款修好后,钱已经结算给供货卖家了,担责的人本来就该是他。
|
||||
|
||||
**改法**:把"从工单反查供货卖家"抽成共享查询
|
||||
`getConsignmentSupplierByWorkOrderWithClient()`(`consignment-supplier-repo.ts`),
|
||||
货款结算与售后立案共用同一份口径(工单 `material_json.consignment.inventoryId`
|
||||
或库存 `bound_work_order_id` → 库存 → 提报单),售后在找不到接单人时回落到供货卖家,
|
||||
并在验收快照里标记 `consignmentSupplier: true` 与提报单号留痕。
|
||||
|
||||
**真实库验证**(内部现货履约,接单人为空):
|
||||
|
||||
| 步骤 | 结果 |
|
||||
| --- | --- |
|
||||
| 从工单反查供货卖家 | `workerId=3`、结算价 12400、提报单号正确 ✅(不是无关的打手 4) |
|
||||
| 售后立案 | ✅ 成功,`case_no=VERIFY-AS-…`,责任人 `worker_id=3`(供货卖家) |
|
||||
|
||||
临时数据已清理(残留 0)。至此"寄卖单没有接单人"这一类坑的三处(货款、售后、卖家按钮)都已收口。
|
||||
|
||||
### 批次 Q · 补上寄卖**货款**结算(本轮)
|
||||
|
||||
**问题(用户对账发现)**:工单 #100171 验收完成时,卖家钱包只有两笔——发布押金 -20 又 +20。
|
||||
|
||||
Reference in New Issue
Block a user