优化接单平台工单查询性能
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
+34
-39
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user