优化接单平台工单查询性能

This commit is contained in:
yml2213
2026-09-04 21:22:35 +08:00
parent 625df52992
commit 84a8f138d4
7 changed files with 158 additions and 82 deletions
@@ -0,0 +1,23 @@
-- 修复接单工单搜索表达式索引漂移。
-- 部署中曾存在索引表达式与当前查询不一致的数据库;新增迁移确保已执行 031 的环境也会重建。
DROP INDEX IF EXISTS idx_work_orders_search_text_trgm;
CREATE INDEX idx_work_orders_search_text_trgm
ON work_orders USING gin (
LOWER(
COALESCE(work_order_no, '') || ' ' ||
COALESCE(platform_order_id, '') || ' ' ||
COALESCE(product_name, '') || ' ' ||
COALESCE(material_json->'collect'->'fields'->>'gameId', '') || ' ' ||
COALESCE(material_json->'collect'->'fields'->>'gameNickname', '') || ' ' ||
COALESCE(material_json->'collect'->'fields'->>'gameAccount', '') || ' ' ||
COALESCE(material_json->'collect'->'fields'->>'roleName', '') || ' ' ||
COALESCE(material_json->'fields'->>'gameId', '') || ' ' ||
COALESCE(material_json->'fields'->>'gameNickname', '') || ' ' ||
COALESCE(material_json->>'gameId', '') || ' ' ||
COALESCE(material_json->>'gameNickname', '')
) gin_trgm_ops
);
COMMENT ON INDEX idx_work_orders_search_text_trgm IS
'接单工单模糊搜索:订单号、商品名、游戏 ID/昵称及历史字段';
@@ -418,6 +418,8 @@ export type GrabWorkOrderResult = {
export type ListInput = {
page?: number
pageSize?: number
/** 列表调用方已有同筛选条件的聚合总数时,可跳过独立 COUNT。 */
includeTotal?: boolean
/** 单状态筛选(兼容 worker 用户等其他列表) */
status?: string
/** 支持多个状态同时筛选(空数组表示全部状态) */
@@ -58,13 +58,6 @@ export const WORK_ORDER_SELECT = `
) share_summary ON TRUE
`
// Keep the page and total in one filtered query. This avoids running the same
// joins and share aggregation twice for every list request.
const LIST_WORK_ORDER_SELECT = WORK_ORDER_SELECT.replace(
' SELECT\n',
' SELECT\n COUNT(*) OVER()::int AS total_count,\n',
)
type WorkerOrderOverviewRow = {
status: string
order_count: number
@@ -234,6 +227,7 @@ export async function listWorkOrdersByPlatformOrderId(
export async function listWorkOrders({
page = 1,
pageSize = 20,
includeTotal = true,
statuses = [],
status = '',
workOrderId = 0,
@@ -271,10 +265,13 @@ export async function listWorkOrders({
vipEvidencePending,
acceptanceMode,
giftPhase,
useOperationalStatus,
// 无状态筛选时运营状态不影响列表入选,不应为此聚合整张拼单表。
useOperationalStatus:
useOperationalStatus && (effectiveStatuses.length > 0 || vipEvidencePending),
createdFrom,
createdTo,
})
const countParams = [...params]
const offset = (page - 1) * pageSize
const orderBy =
sort === 'worker_claimed_at_desc' && workerSharingId
@@ -289,33 +286,42 @@ export async function listWorkOrders({
: 'wo.id DESC'
const effectiveOrderBy = pinnedFirst ? `wo.pinned_at DESC NULLS LAST, ${orderBy}` : orderBy
params.push(pageSize, offset)
const itemsResult = await query<WorkOrderRow>(
`${LIST_WORK_ORDER_SELECT}
${joins}
${whereClause}
ORDER BY ${effectiveOrderBy}
LIMIT $${params.length - 1} OFFSET $${params.length}`,
params,
)
// A page beyond the end has no row from which to read the window count.
// Keep that uncommon pagination edge case correct without restoring a
// second query for normal requests.
let total = Number(
(itemsResult.rows[0] as WorkOrderRow & { total_count?: number })?.total_count || 0,
)
if (itemsResult.rows.length === 0 && page > 1) {
const totalResult = await query<{ total: number }>(
`SELECT COUNT(*)::int AS total FROM work_orders wo ${joins} ${whereClause}`,
params.slice(0, -2),
)
total = Number(totalResult.rows[0]?.total || 0)
}
// 宽详情只查当前页;不将 COUNT 窗口放进详情查询,避免 LIMIT 前处理全量 JSON。
const [itemsResult, total] = await Promise.all([
query<WorkOrderRow>(
`${WORK_ORDER_SELECT}
${joins}
${whereClause}
ORDER BY ${effectiveOrderBy}
LIMIT $${params.length - 1} OFFSET $${params.length}`,
params,
),
includeTotal ? countWorkOrdersWithWhere(joins, whereClause, countParams) : Promise.resolve(0),
])
return {
items: itemsResult.rows,
total,
}
}
/** 只返回筛选范围总数,不加载工单 JSON、取消事件和当前页关联字段。 */
export async function countWorkOrders(input: ListInput = {}): Promise<number> {
const { joins, whereClause, params } = buildWorkOrderWhere(input)
return countWorkOrdersWithWhere(joins, whereClause, params)
}
async function countWorkOrdersWithWhere(
joins: string,
whereClause: string,
params: unknown[],
): Promise<number> {
const result = await query<{ total: number }>(
`SELECT COUNT(*)::int AS total FROM work_orders wo ${joins} ${whereClause}`,
params,
)
return Number(result.rows[0]?.total || 0)
}
/** 按筛选条件汇总工单,统计范围不受当前分页影响。 */
export async function getWorkOrderStatistics(input: ListInput = {}): Promise<WorkOrderStatistics> {
const { joins, whereClause, params } = buildWorkOrderWhere(input)
@@ -55,12 +55,15 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
...(createdFrom ? { createdFrom } : {}),
...(createdTo ? { createdTo } : {}),
}
const { items, total } = await listWorkOrders({ page, pageSize, ...filters })
const statistics = await getWorkOrderStatistics(filters)
const pendingUnfreezeByOrderId = await sumPendingUnfreezeByOrderIds(
items.map((item) => Number(item.id)),
)
const shares = await listWorkOrderSharesByOrderIds(items.map((item) => Number(item.id)))
const [{ items }, statistics] = await Promise.all([
listWorkOrders({ page, pageSize, includeTotal: false, ...filters }),
getWorkOrderStatistics(filters),
])
const itemIds = items.map((item) => Number(item.id))
const [pendingUnfreezeByOrderId, shares] = await Promise.all([
sumPendingUnfreezeByOrderIds(itemIds),
listWorkOrderSharesByOrderIds(itemIds),
])
const sharesByOrderId = new Map<number, typeof shares>()
for (const share of shares) {
const orderId = Number(share.work_order_id)
@@ -73,7 +76,7 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
...mapWorkOrderAdmin(item, sharesByOrderId.get(Number(item.id)) || []),
pendingUnfreezeAmount: pendingUnfreezeByOrderId.get(Number(item.id)) || 0,
})),
pagination: { page, pageSize, total },
pagination: { page, pageSize, total: statistics.totalCount },
statistics,
}
}
@@ -1,57 +1,52 @@
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
import { listWorkOrders, listWorkerUsers } from '../../repositories/worker-platform/index.js'
import { countWorkOrders, listWorkerUsers } from '../../repositories/worker-platform/index.js'
import { publishWorkerHallRealtimeRefresh } from '../realtime/realtime-event-service.js'
import { getWorkerHallConfig, saveWorkerHallConfig } from './worker-hall-config-service.js'
export async function getAdminWorkerPlatformSummary() {
const hallConfig = getWorkerHallConfig()
const [pendingWorkers, pendingMaterial, openOrders, visibleOpenOrders, inProgressOrders] =
await Promise.all([
listWorkerUsers({ page: 1, pageSize: 1, status: 'pending_review' }),
listWorkOrders({ page: 1, pageSize: 1, statuses: [WORK_ORDER_STATUS.PENDING_MATERIAL] }),
listWorkOrders({
page: 1,
pageSize: 1,
statuses: [WORK_ORDER_STATUS.OPEN],
excludeFilledSharing: true,
}),
listWorkOrders({
page: 1,
pageSize: 1,
statuses: [WORK_ORDER_STATUS.OPEN],
hallCandidateLimit: hallConfig.maxVisibleOrders,
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
excludeFilledSharing: true,
}),
listWorkOrders({
page: 1,
pageSize: 1,
statuses: [WORK_ORDER_STATUS.IN_PROGRESS],
useOperationalStatus: true,
}),
])
const [
pendingWorkers,
pendingMaterialCount,
openOrderCount,
visibleOpenOrderCount,
inProgressOrderCount,
] = await Promise.all([
listWorkerUsers({ page: 1, pageSize: 1, status: 'pending_review' }),
countWorkOrders({ statuses: [WORK_ORDER_STATUS.PENDING_MATERIAL] }),
countWorkOrders({
statuses: [WORK_ORDER_STATUS.OPEN],
excludeFilledSharing: true,
}),
countWorkOrders({
statuses: [WORK_ORDER_STATUS.OPEN],
hallCandidateLimit: hallConfig.maxVisibleOrders,
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
excludeFilledSharing: true,
}),
countWorkOrders({
statuses: [WORK_ORDER_STATUS.IN_PROGRESS],
useOperationalStatus: true,
}),
])
return {
pendingWorkerCount: pendingWorkers.total,
pendingMaterialCount: pendingMaterial.total,
openOrderCount: visibleOpenOrders.total,
queuedHallOrderCount: Math.max(0, openOrders.total - visibleOpenOrders.total),
pendingMaterialCount,
openOrderCount: visibleOpenOrderCount,
queuedHallOrderCount: Math.max(0, openOrderCount - visibleOpenOrderCount),
hallMaxVisibleOrders: hallConfig.maxVisibleOrders,
inProgressOrderCount: inProgressOrders.total,
inProgressOrderCount,
}
}
export async function getAdminWorkerHallConfig() {
const config = getWorkerHallConfig()
const [openOrders, visibleOpenOrders] = await Promise.all([
listWorkOrders({
page: 1,
pageSize: 1,
const [openOrderCount, visibleOpenOrderCount] = await Promise.all([
countWorkOrders({
statuses: [WORK_ORDER_STATUS.OPEN],
excludeFilledSharing: true,
}),
listWorkOrders({
page: 1,
pageSize: 1,
countWorkOrders({
statuses: [WORK_ORDER_STATUS.OPEN],
hallCandidateLimit: config.maxVisibleOrders,
hallCategoryLimits: config.categoryMaxVisibleOrders,
@@ -60,8 +55,8 @@ export async function getAdminWorkerHallConfig() {
])
return {
...config,
visibleOrderCount: visibleOpenOrders.total,
queuedOrderCount: Math.max(0, openOrders.total - visibleOpenOrders.total),
visibleOrderCount: visibleOpenOrderCount,
queuedOrderCount: Math.max(0, openOrderCount - visibleOpenOrderCount),
}
}