feat(category): 接单平台支持二级分类目录与接单大厅联动展示及限额优化

This commit is contained in:
yml2213
2026-09-12 18:45:56 +08:00
parent 111f400bdb
commit d1e06f362e
17 changed files with 819 additions and 135 deletions
@@ -256,6 +256,8 @@ export type WorkCategoryRow = {
id: number
category_key: string
name: string
parent_id?: number | null
icon_url?: string | null
sort_order: number
status: string
created_at: string
@@ -402,6 +404,8 @@ export type WorkOrderRow = {
active_share_quantity?: number
pending_share_count?: number
category_name?: string
parent_category_id?: number | null
parent_category_name?: string
worker_username?: string
worker_display_name?: string
/** 取消信息,由工单查询从取消类事件推导,仅已取消工单有值。 */
@@ -36,19 +36,30 @@ export async function listAllWorkCategories(): Promise<WorkCategoryRow[]> {
export async function upsertWorkCategory(input: {
categoryKey: string
name: string
parentId?: number | null
iconUrl?: string | null
sortOrder: number
status: string
now: string
}): Promise<WorkCategoryRow | null> {
const result = await query<WorkCategoryRow>(
`
INSERT INTO work_categories (category_key, name, sort_order, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6)
INSERT INTO work_categories (category_key, name, parent_id, icon_url, sort_order, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (category_key) DO UPDATE
SET name = EXCLUDED.name, sort_order = EXCLUDED.sort_order, status = EXCLUDED.status, updated_at = EXCLUDED.updated_at
SET name = EXCLUDED.name, parent_id = EXCLUDED.parent_id, icon_url = EXCLUDED.icon_url, sort_order = EXCLUDED.sort_order, status = EXCLUDED.status, updated_at = EXCLUDED.updated_at
RETURNING *
`,
[input.categoryKey, input.name, input.sortOrder, input.status, input.now, input.now],
[
input.categoryKey,
input.name,
input.parentId ?? null,
input.iconUrl ?? null,
input.sortOrder,
input.status,
input.now,
input.now,
],
)
return result.rows[0] || null
}
@@ -56,8 +67,9 @@ export async function upsertWorkCategory(input: {
export async function countWorkCategoryUsages(categoryId: number | string): Promise<{
workOrderCount: number
productRuleCount: number
childCategoryCount: number
}> {
const [orders, rules] = await Promise.all([
const [orders, rules, children] = await Promise.all([
query<{ total: number }>(
'SELECT COUNT(*)::int AS total FROM work_orders WHERE category_id = $1',
[Number(categoryId)],
@@ -66,10 +78,15 @@ export async function countWorkCategoryUsages(categoryId: number | string): Prom
'SELECT COUNT(*)::int AS total FROM work_product_rules WHERE category_id = $1',
[Number(categoryId)],
),
query<{ total: number }>(
'SELECT COUNT(*)::int AS total FROM work_categories WHERE parent_id = $1',
[Number(categoryId)],
),
])
return {
workOrderCount: Number(orders.rows[0]?.total || 0),
productRuleCount: Number(rules.rows[0]?.total || 0),
childCategoryCount: Number(children.rows[0]?.total || 0),
}
}
@@ -11,6 +11,8 @@ export const WORK_ORDER_SELECT = `
COALESCE(share_summary.active_share_quantity, 0)::int AS active_share_quantity,
COALESCE(share_summary.pending_share_count, 0)::int AS pending_share_count,
wc.name AS category_name,
parent_wc.id AS parent_category_id,
parent_wc.name AS parent_category_name,
wu.username AS worker_username,
wu.display_name AS worker_display_name,
cancel_info.cancel_reason,
@@ -20,6 +22,7 @@ export const WORK_ORDER_SELECT = `
cancel_info.cancelled_at
FROM work_orders wo
LEFT JOIN work_categories wc ON wc.id = wo.category_id
LEFT JOIN work_categories parent_wc ON parent_wc.id = wc.parent_id
LEFT JOIN worker_users wu ON wu.id = wo.assigned_worker_id
LEFT JOIN LATERAL (
SELECT
@@ -862,7 +865,9 @@ export function buildWorkOrderWhere({
}
if (categoryId) {
params.push(categoryId)
filters.push(`wo.category_id = $${params.length}`)
filters.push(
`(wo.category_id = $${params.length} OR wo.category_id IN (SELECT id FROM work_categories WHERE parent_id = $${params.length}))`,
)
}
if (acceptanceMode) {
params.push(normalizeWorkOrderAcceptanceMode(acceptanceMode))
@@ -917,12 +922,21 @@ function buildHallCandidateIdsSql(limitPlaceholder: string, categoryLimitsPlaceh
return `
SELECT ranked.id
FROM (
SELECT candidate.id, candidate.category_id, candidate.hall_queued_at,
SELECT
candidate.id,
candidate.category_id,
candidate.hall_queued_at,
COALESCE(candidate_wc.parent_id, candidate.category_id) AS primary_category_id,
ROW_NUMBER() OVER (
PARTITION BY COALESCE(candidate_wc.parent_id, candidate.category_id)
ORDER BY candidate.hall_queued_at ASC NULLS LAST, candidate.id ASC
) AS primary_category_rank,
ROW_NUMBER() OVER (
PARTITION BY candidate.category_id
ORDER BY candidate.hall_queued_at ASC NULLS LAST, candidate.id ASC
) AS category_rank
) AS sub_category_rank
FROM work_orders candidate
LEFT JOIN work_categories candidate_wc ON candidate_wc.id = candidate.category_id
LEFT JOIN (
SELECT
work_order_id,
@@ -936,10 +950,15 @@ function buildHallCandidateIdsSql(limitPlaceholder: string, categoryLimitsPlaceh
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,
WHERE ranked.primary_category_rank <= COALESCE(
NULLIF(${categoryLimitsPlaceholder}::jsonb ->> ranked.primary_category_id::text, '')::int,
${limitPlaceholder}
)
AND (
NULLIF(${categoryLimitsPlaceholder}::jsonb ->> ranked.category_id::text, '') IS NULL
OR ranked.category_id = ranked.primary_category_id
OR ranked.sub_category_rank <= NULLIF(${categoryLimitsPlaceholder}::jsonb ->> ranked.category_id::text, '')::int
)
ORDER BY ranked.hall_queued_at ASC NULLS LAST, ranked.id ASC
LIMIT ${limitPlaceholder}
`