增加接单分类和大厅分类配额

This commit is contained in:
yml2213
2026-08-23 17:37:32 +08:00
parent 844f071aab
commit 1cebdd0487
18 changed files with 297 additions and 61 deletions
@@ -101,5 +101,21 @@ test('大厅容量先确定全局候选集合,再应用页面筛选条件', ()
assert.match(query.whereClause, /LIMIT \$4/)
const candidateSql = query.whereClause.slice(query.whereClause.indexOf('candidate.status'))
assert.doesNotMatch(candidateSql, /candidate\.category_id|candidate.*测试商品/)
assert.deepEqual(query.params, [['open'], '%测试商品%', 3, 50, '2026-08-21T00:00:00.000Z'])
assert.deepEqual(query.params, [['open'], '%测试商品%', 3, 50, '{}', '2026-08-21T00:00:00.000Z'])
})
test('后台接单工单支持按接单分类筛选', () => {
const query = buildWorkOrderWhere({ categoryId: 7 })
assert.match(query.whereClause, /wo\.category_id = \$1/)
assert.deepEqual(query.params, [7])
})
test('大厅候选按分类上限取队首后再受全局上限限制', () => {
const query = buildWorkOrderWhere({
hallCandidateLimit: 20,
hallCategoryLimits: { '1': 3, '2': 5 },
})
assert.match(query.whereClause, /ROW_NUMBER\(\) OVER/)
assert.match(query.whereClause, /category_rank/)
assert.deepEqual(query.params, [20, '{"1":3,"2":5}'])
})
@@ -431,6 +431,7 @@ export type ListInput = {
workerSharingId?: number
/** 大厅候选集合的总展示上限;筛选与分页在候选集合内执行。 */
hallCandidateLimit?: number
hallCategoryLimits?: Record<string, number>
visibleAfterIso?: string
/** 抢单大厅隐藏已被拼完的工单。 */
excludeFilledSharing?: boolean
@@ -14,6 +14,7 @@ export async function grabWorkOrder(input: {
depositAmount: number
maxActiveOrders: number
hallCandidateLimit?: number
hallCategoryLimits?: Record<string, number>
visibleAfterIso?: string
deadlineAt: string | null
now: string
@@ -42,6 +43,7 @@ export async function grabWorkOrder(input: {
!(await isWorkOrderWithinHallCapacityWithClient(client, {
workOrderId: input.workOrderId,
hallCandidateLimit: input.hallCandidateLimit,
...(input.hallCategoryLimits ? { hallCategoryLimits: input.hallCategoryLimits } : {}),
visibleAfterIso: input.visibleAfterIso,
}))
) {
@@ -226,6 +226,7 @@ export async function listWorkOrders({
categoryId = 0,
workerSharingId = 0,
hallCandidateLimit = 0,
hallCategoryLimits = {},
visibleAfterIso = '',
excludeFilledSharing = false,
vipEvidencePending = false,
@@ -247,6 +248,7 @@ export async function listWorkOrders({
categoryId,
workerSharingId,
hallCandidateLimit,
hallCategoryLimits,
visibleAfterIso,
excludeFilledSharing,
vipEvidencePending,
@@ -652,6 +654,7 @@ export function buildWorkOrderWhere({
categoryId = 0,
workerSharingId = 0,
hallCandidateLimit = 0,
hallCategoryLimits = {},
visibleAfterIso = '',
excludeFilledSharing = false,
vipEvidencePending = false,
@@ -843,7 +846,9 @@ export function buildWorkOrderWhere({
}
if (hallCandidateLimit > 0) {
params.push(hallCandidateLimit)
filters.push(`wo.id IN (${buildHallCandidateIdsSql(`$${params.length}`)})`)
const limitPlaceholder = `$${params.length}`
params.push(JSON.stringify(hallCategoryLimits))
filters.push(`wo.id IN (${buildHallCandidateIdsSql(limitPlaceholder, `$${params.length}`)})`)
}
if (visibleAfterIso) {
params.push(visibleAfterIso)
@@ -863,23 +868,34 @@ export function buildWorkOrderWhere({
}
/** 大厅容量先选出全局队首,分类、关键词和 VIP 延迟只能在该集合内继续筛选。 */
function buildHallCandidateIdsSql(limitPlaceholder: string) {
function buildHallCandidateIdsSql(limitPlaceholder: string, categoryLimitsPlaceholder: string) {
return `
SELECT candidate.id
FROM work_orders candidate
LEFT JOIN (
SELECT
work_order_id,
SUM(quantity) FILTER (WHERE status != 'cancelled') AS active_share_quantity
FROM work_order_shares
GROUP BY work_order_id
) candidate_shares ON candidate_shares.work_order_id = candidate.id
WHERE candidate.status = 'open'
AND (
candidate.sharing_enabled IS NOT TRUE
OR COALESCE(candidate_shares.active_share_quantity, 0) < candidate.sharing_total_quantity
)
ORDER BY candidate.hall_queued_at ASC NULLS LAST, candidate.id ASC
SELECT ranked.id
FROM (
SELECT candidate.id, candidate.category_id, candidate.hall_queued_at,
ROW_NUMBER() OVER (
PARTITION BY candidate.category_id
ORDER BY candidate.hall_queued_at ASC NULLS LAST, candidate.id ASC
) AS category_rank
FROM work_orders candidate
LEFT JOIN (
SELECT
work_order_id,
SUM(quantity) FILTER (WHERE status != 'cancelled') AS active_share_quantity
FROM work_order_shares
GROUP BY work_order_id
) candidate_shares ON candidate_shares.work_order_id = candidate.id
WHERE candidate.status = 'open'
AND (
candidate.sharing_enabled IS NOT TRUE
OR COALESCE(candidate_shares.active_share_quantity, 0) < candidate.sharing_total_quantity
)
) ranked
WHERE ranked.category_rank <= COALESCE(
NULLIF(${categoryLimitsPlaceholder}::jsonb ->> ranked.category_id::text, '')::int,
${limitPlaceholder}
)
ORDER BY ranked.hall_queued_at ASC NULLS LAST, ranked.id ASC
LIMIT ${limitPlaceholder}
`
}
@@ -887,7 +903,12 @@ function buildHallCandidateIdsSql(limitPlaceholder: string) {
/** 抢单操作在事务中复核容量与当前打手的可见延迟,避免通过订单 ID 绕过大厅队列。 */
export async function isWorkOrderWithinHallCapacityWithClient(
client: PoolClient,
input: { workOrderId: number; hallCandidateLimit: number; visibleAfterIso?: string | undefined },
input: {
workOrderId: number
hallCandidateLimit: number
hallCategoryLimits?: Record<string, number>
visibleAfterIso?: string | undefined
},
) {
const result = await client.query<{ visible: boolean }>(
`
@@ -895,13 +916,14 @@ export async function isWorkOrderWithinHallCapacityWithClient(
SELECT 1
FROM work_orders wo
WHERE wo.id = $1
AND wo.id IN (${buildHallCandidateIdsSql('$2')})
AND ($3::timestamptz IS NULL OR wo.hall_queued_at IS NULL OR wo.hall_queued_at <= $3)
AND wo.id IN (${buildHallCandidateIdsSql('$2', '$3')})
AND ($4::timestamptz IS NULL OR wo.hall_queued_at IS NULL OR wo.hall_queued_at <= $4)
) AS visible
`,
[
input.workOrderId,
input.hallCandidateLimit,
JSON.stringify(input.hallCategoryLimits || {}),
input.visibleAfterIso ? input.visibleAfterIso : null,
],
)
@@ -103,6 +103,7 @@ export async function joinWorkOrderShare(input: {
takeRemaining?: boolean
maxActiveOrders: number
hallCandidateLimit?: number
hallCategoryLimits?: Record<string, number>
visibleAfterIso?: string
now: string
}): Promise<{
@@ -137,6 +138,7 @@ export async function joinWorkOrderShare(input: {
!(await isWorkOrderWithinHallCapacityWithClient(client, {
workOrderId: input.workOrderId,
hallCandidateLimit: input.hallCandidateLimit,
...(input.hallCategoryLimits ? { hallCategoryLimits: input.hallCategoryLimits } : {}),
visibleAfterIso: input.visibleAfterIso,
}))
) {