fix(consignment): 内部库存履约后不再把买家资料暴露给上一手打手
退回大厅留下的 last_assigned 在状态离开待接单后会重新可见。履约时摘掉上一手,并清掉已经漏出的内部现货工单。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
-- 内部现货履约没有接单打手。退回大厅后留下的 last_assigned_worker_id
|
||||
-- 会在状态离开 open 后,把买家资料重新暴露给上一手打手。
|
||||
-- 判定与 hasInternalConsignmentFulfillment 一致:无当前指派、挂着寄卖库存、不是押金履约、没有供货打手。
|
||||
UPDATE work_orders
|
||||
SET
|
||||
last_assigned_worker_id = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE assigned_worker_id IS NULL
|
||||
AND last_assigned_worker_id IS NOT NULL
|
||||
AND COALESCE(material_json #>> '{consignment,inventoryId}', '') <> ''
|
||||
AND COALESCE(material_json #>> '{consignment,fulfillmentType}', '') <> 'worker_deposit'
|
||||
AND COALESCE(NULLIF(material_json #>> '{consignment,workerId}', ''), '0') = '0';
|
||||
@@ -173,6 +173,22 @@ test('普通关键词仍然走 trgm 表达式模糊搜索', () => {
|
||||
assert.deepEqual(query.params, ['%指挥官密钥%'])
|
||||
})
|
||||
|
||||
test('内部现货不再因上一手 last_assigned 进入打手可见集', async () => {
|
||||
const query = buildWorkOrderWhere({ workerSharingId: 183, statuses: ['in_progress'] })
|
||||
assert.match(query.joins, /historical\.last_assigned_worker_id = \$1/)
|
||||
assert.match(query.joins, /consignment,fulfillmentType/)
|
||||
assert.match(query.joins, /worker_deposit/)
|
||||
assert.match(query.joins, /historical\.assigned_worker_id IS NULL/)
|
||||
|
||||
const { readFile } = await import('node:fs/promises')
|
||||
const src = await readFile(
|
||||
new URL('./worker-platform/work-order-query-repo.ts', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const calls = src.match(/retainedWorkerOrderPredicate\('/g) || []
|
||||
assert.equal(calls.length, 2, '列表和概览必须共用同一条历史可见条件')
|
||||
})
|
||||
|
||||
test('「仅问题单」检索走短路分支,从 pending 表反查而非枚举可见集', () => {
|
||||
const query = buildWorkOrderWhere({ workerSharingId: 7, statuses: ['problem'] })
|
||||
|
||||
|
||||
@@ -96,6 +96,25 @@ export function hasInternalConsignmentFulfillment(workOrder: {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 打手是否还能把这张工单当成自己的单。
|
||||
*
|
||||
* 当前指派优先。退回大厅后留下的 last_assigned_worker_id 只用于回看自己做过的单;
|
||||
* 一旦改走内部现货,这张单没有接单打手,上一手不能再看到买家资料。
|
||||
*/
|
||||
export function workerOwnsWorkOrder(
|
||||
workOrder: {
|
||||
assigned_worker_id?: number | null
|
||||
last_assigned_worker_id?: number | null
|
||||
material_json?: unknown
|
||||
},
|
||||
workerId: number,
|
||||
): boolean {
|
||||
if (Number(workOrder.assigned_worker_id || 0) === workerId) return true
|
||||
if (Number(workOrder.last_assigned_worker_id || 0) !== workerId) return false
|
||||
return !hasInternalConsignmentFulfillment(workOrder)
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部寄售现货履约落地:占库成功,但**不**算已加好友。
|
||||
*
|
||||
@@ -150,6 +169,9 @@ export async function parkWorkOrderWaitingFriendAddWithClient(
|
||||
friend_added_at = NULL,
|
||||
gift_available_at = NULL,
|
||||
deadline_at = NULL,
|
||||
-- 内部现货没有接单打手。退回大厅留下的上一手必须摘掉,
|
||||
-- 否则状态一离开 open,他的「我的订单」会重新带出买家资料。
|
||||
last_assigned_worker_id = NULL,
|
||||
accepted_at = CASE WHEN $4 THEN NULL ELSE accepted_at END,
|
||||
material_json = COALESCE($5::jsonb, material_json),
|
||||
reward_amount = COALESCE($7, reward_amount),
|
||||
|
||||
@@ -513,6 +513,34 @@ export async function getWorkOrderStatistics(input: ListInput = {}): Promise<Wor
|
||||
return statistics
|
||||
}
|
||||
|
||||
/**
|
||||
* 上一手打手仍可回看的历史单。
|
||||
*
|
||||
* 大厅中的单(status=open)默认不展示,除非是超时退回。
|
||||
* 内部现货没有接单打手:退回大厅留下的 last_assigned_worker_id 不能在履约改走库存后继续暴露买家资料。
|
||||
* 判定与 hasInternalConsignmentFulfillment 一致。
|
||||
*/
|
||||
export function retainedWorkerOrderPredicate(alias: string, workerParam: string): string {
|
||||
return `
|
||||
${alias}.last_assigned_worker_id = ${workerParam}
|
||||
AND (
|
||||
${alias}.status <> 'open'
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM work_order_events timeout_event
|
||||
WHERE timeout_event.work_order_id = ${alias}.id
|
||||
AND timeout_event.event_type = 'timeout_reopen'
|
||||
)
|
||||
)
|
||||
AND NOT (
|
||||
${alias}.assigned_worker_id IS NULL
|
||||
AND COALESCE(${alias}.material_json #>> '{consignment,inventoryId}', '') <> ''
|
||||
AND COALESCE(${alias}.material_json #>> '{consignment,fulfillmentType}', '') <> 'worker_deposit'
|
||||
AND COALESCE(NULLIF(${alias}.material_json #>> '{consignment,workerId}', ''), '0') = '0'
|
||||
)
|
||||
`
|
||||
}
|
||||
|
||||
/** 汇总打手本人各订单状态的可结算金额,拼单按个人份额计算。 */
|
||||
export async function getWorkerOrderOverview(workerId: number | string): Promise<
|
||||
Array<{
|
||||
@@ -533,16 +561,7 @@ export async function getWorkerOrderOverview(workerId: number | string): Promise
|
||||
UNION
|
||||
SELECT wo.id AS work_order_id
|
||||
FROM work_orders wo
|
||||
WHERE wo.last_assigned_worker_id = $1
|
||||
AND (
|
||||
wo.status <> 'open'
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM work_order_events timeout_event
|
||||
WHERE timeout_event.work_order_id = wo.id
|
||||
AND timeout_event.event_type = 'timeout_reopen'
|
||||
)
|
||||
)
|
||||
WHERE ${retainedWorkerOrderPredicate('wo', '$1')}
|
||||
UNION
|
||||
SELECT wos.work_order_id
|
||||
FROM work_order_shares wos
|
||||
@@ -909,16 +928,7 @@ export function buildWorkOrderWhere({
|
||||
UNION
|
||||
SELECT historical.id AS work_order_id
|
||||
FROM work_orders historical
|
||||
WHERE historical.last_assigned_worker_id = ${workerSharePlaceholder}
|
||||
AND (
|
||||
historical.status <> 'open'
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM work_order_events timeout_event
|
||||
WHERE timeout_event.work_order_id = historical.id
|
||||
AND timeout_event.event_type = 'timeout_reopen'
|
||||
)
|
||||
)
|
||||
WHERE ${retainedWorkerOrderPredicate('historical', workerSharePlaceholder)}
|
||||
UNION
|
||||
SELECT share_source.work_order_id
|
||||
FROM work_order_shares share_source
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { PoolClient } from 'pg'
|
||||
|
||||
import { query, withTransaction } from '../../db/client.js'
|
||||
import { createWorkOrderEventWithClient } from './work-order-event-repo.js'
|
||||
import { workerOwnsWorkOrder } from './work-order-gift-repo.js'
|
||||
import { getWorkOrderByIdWithClient } from './work-order-repo.js'
|
||||
import type { WorkerOrderFeedbackRow, WorkOrderRow, WorkOrderShareRow } from './types.js'
|
||||
|
||||
@@ -79,10 +80,9 @@ export async function createWorkerOrderFeedback(input: {
|
||||
[input.workOrderId, input.workerId],
|
||||
)
|
||||
const share = shareResult.rows[0] || null
|
||||
const isOrderOwner =
|
||||
Number(order.assigned_worker_id || 0) === input.workerId ||
|
||||
Number(order.last_assigned_worker_id || 0) === input.workerId
|
||||
if (!share && !isOrderOwner) return { feedback: null, failureReason: 'owner_required' }
|
||||
if (!share && !workerOwnsWorkOrder(order, input.workerId)) {
|
||||
return { feedback: null, failureReason: 'owner_required' }
|
||||
}
|
||||
|
||||
const result = await client.query<{ id: number }>(
|
||||
`INSERT INTO worker_order_feedbacks (
|
||||
|
||||
@@ -599,7 +599,13 @@ export async function fulfillWorkOrderWithConsignment(input: {
|
||||
}
|
||||
}
|
||||
|
||||
publishWorkOrderRealtimeChange({ workOrderId: Number(updatedOrder.id), hallChanged: true })
|
||||
publishWorkOrderRealtimeChange({
|
||||
workOrderId: Number(updatedOrder.id),
|
||||
hallChanged: true,
|
||||
// 履约会清掉 last_assigned_worker_id,广播时工单上已经没有上一手,
|
||||
// 要显式带上他,否则「我的订单」缓存最长 30 秒还显示这张单。
|
||||
workerIds: [Number(workOrder.last_assigned_worker_id || 0)].filter((id) => id > 0),
|
||||
})
|
||||
|
||||
return {
|
||||
order: mapWorkOrderAdmin(updatedOrder),
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
listWorkOrderEventsByOrderId,
|
||||
listWorkOrders,
|
||||
markWorkerWorkOrderViewed,
|
||||
workerOwnsWorkOrder,
|
||||
upsertWorkerWorkOrderNote,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
@@ -181,10 +182,7 @@ export async function getWorkerMyOrderEvents(workOrderId: number | string, sessi
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
const workerId = Number(session.workerId)
|
||||
const sharingShare = await getWorkOrderShare(workOrder.id, workerId)
|
||||
const isOrderOwner =
|
||||
Number(workOrder.assigned_worker_id || 0) === workerId ||
|
||||
Number(workOrder.last_assigned_worker_id || 0) === workerId
|
||||
if (!sharingShare && !isOrderOwner) {
|
||||
if (!sharingShare && !workerOwnsWorkOrder(workOrder, workerId)) {
|
||||
throw createHttpError('只能查看自己订单的流转记录', {
|
||||
statusCode: 403,
|
||||
errorCode: 'work_order_events_owner_required',
|
||||
@@ -345,10 +343,7 @@ export async function saveWorkerOrderNote(
|
||||
|
||||
const worker = await getRequiredWorker(workerId)
|
||||
const sharingShare = await getWorkOrderShare(workOrder.id, workerId)
|
||||
const isOrderOwner =
|
||||
Number(workOrder.assigned_worker_id || 0) === workerId ||
|
||||
Number(workOrder.last_assigned_worker_id || 0) === workerId
|
||||
if (!sharingShare && !isOrderOwner) {
|
||||
if (!sharingShare && !workerOwnsWorkOrder(workOrder, workerId)) {
|
||||
throw createHttpError('只能修改自己的订单备注', {
|
||||
statusCode: 403,
|
||||
errorCode: 'work_order_note_owner_required',
|
||||
@@ -380,10 +375,7 @@ export async function markWorkerOrderRead(
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
const workerId = Number(session.workerId)
|
||||
const sharingShare = await getWorkOrderShare(workOrder.id, workerId)
|
||||
const isOrderOwner =
|
||||
Number(workOrder.assigned_worker_id || 0) === workerId ||
|
||||
Number(workOrder.last_assigned_worker_id || 0) === workerId
|
||||
if (!sharingShare && !isOrderOwner) {
|
||||
if (!sharingShare && !workerOwnsWorkOrder(workOrder, workerId)) {
|
||||
throw createHttpError('只能标记自己的订单为已读', {
|
||||
statusCode: 403,
|
||||
errorCode: 'work_order_read_owner_required',
|
||||
|
||||
@@ -73,7 +73,10 @@ import {
|
||||
} from '../../repositories/worker-platform/consignment-repo.js'
|
||||
import { ADMIN_PERMISSION_KEYS } from '../admin/admin-rbac-service.js'
|
||||
import { updateAdminConsignmentInventory } from './consignment-admin-service.js'
|
||||
import { parkWorkOrderWaitingFriendAddWithClient } from '../../repositories/worker-platform/work-order-gift-repo.js'
|
||||
import {
|
||||
parkWorkOrderWaitingFriendAddWithClient,
|
||||
workerOwnsWorkOrder,
|
||||
} from '../../repositories/worker-platform/work-order-gift-repo.js'
|
||||
import {
|
||||
assertConsignmentRevocableWorkOrderStatus,
|
||||
resolveRevocableConsignmentInventoryId,
|
||||
@@ -2308,6 +2311,10 @@ test('内部现货履约停在「待加好友」:不写 friend_added_at,也
|
||||
assert.ok(sql.includes('friend_added_at = NULL'), '不能自动确认加好友')
|
||||
assert.ok(sql.includes('gift_available_at = NULL'))
|
||||
assert.ok(sql.includes('deadline_at = NULL'), '没有打手在跑,不应背任务时限')
|
||||
assert.ok(
|
||||
sql.includes('last_assigned_worker_id = NULL'),
|
||||
'内部现货必须摘掉上一手打手,否则状态离开 open 后他会重新看到买家资料',
|
||||
)
|
||||
assert.ok(sql.includes("acceptance_mode = 'friend_gift'"))
|
||||
assert.ok(!sql.includes('friend_added_at = $2'), '不能把当前时间写成已加好友')
|
||||
|
||||
@@ -2321,6 +2328,34 @@ test('内部现货履约停在「待加好友」:不写 friend_added_at,也
|
||||
)
|
||||
})
|
||||
|
||||
test('内部现货履约后,上一手打手不再拥有这张工单', () => {
|
||||
const internalOrder = {
|
||||
assigned_worker_id: null,
|
||||
last_assigned_worker_id: 183,
|
||||
material_json: {
|
||||
consignment: { inventoryId: 414, fulfillmentType: 'internal_stock' },
|
||||
},
|
||||
}
|
||||
assert.equal(workerOwnsWorkOrder(internalOrder, 183), false)
|
||||
assert.equal(
|
||||
workerOwnsWorkOrder({ ...internalOrder, assigned_worker_id: 183 }, 183),
|
||||
true,
|
||||
'重新指派给本人后仍然是他的单',
|
||||
)
|
||||
assert.equal(
|
||||
workerOwnsWorkOrder(
|
||||
{
|
||||
assigned_worker_id: null,
|
||||
last_assigned_worker_id: 183,
|
||||
material_json: {},
|
||||
},
|
||||
183,
|
||||
),
|
||||
true,
|
||||
'普通退回后的历史单仍可回看',
|
||||
)
|
||||
})
|
||||
|
||||
test('停在待加好友时保留商品规则已配的冷却时长', async () => {
|
||||
const calls: Array<{ sql: string; values?: unknown[] }> = []
|
||||
const client = {
|
||||
|
||||
Reference in New Issue
Block a user