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

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/) assert.match(query.whereClause, /LIMIT \$4/)
const candidateSql = query.whereClause.slice(query.whereClause.indexOf('candidate.status')) const candidateSql = query.whereClause.slice(query.whereClause.indexOf('candidate.status'))
assert.doesNotMatch(candidateSql, /candidate\.category_id|candidate.*测试商品/) 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 workerSharingId?: number
/** 大厅候选集合的总展示上限;筛选与分页在候选集合内执行。 */ /** 大厅候选集合的总展示上限;筛选与分页在候选集合内执行。 */
hallCandidateLimit?: number hallCandidateLimit?: number
hallCategoryLimits?: Record<string, number>
visibleAfterIso?: string visibleAfterIso?: string
/** 抢单大厅隐藏已被拼完的工单。 */ /** 抢单大厅隐藏已被拼完的工单。 */
excludeFilledSharing?: boolean excludeFilledSharing?: boolean
@@ -14,6 +14,7 @@ export async function grabWorkOrder(input: {
depositAmount: number depositAmount: number
maxActiveOrders: number maxActiveOrders: number
hallCandidateLimit?: number hallCandidateLimit?: number
hallCategoryLimits?: Record<string, number>
visibleAfterIso?: string visibleAfterIso?: string
deadlineAt: string | null deadlineAt: string | null
now: string now: string
@@ -42,6 +43,7 @@ export async function grabWorkOrder(input: {
!(await isWorkOrderWithinHallCapacityWithClient(client, { !(await isWorkOrderWithinHallCapacityWithClient(client, {
workOrderId: input.workOrderId, workOrderId: input.workOrderId,
hallCandidateLimit: input.hallCandidateLimit, hallCandidateLimit: input.hallCandidateLimit,
...(input.hallCategoryLimits ? { hallCategoryLimits: input.hallCategoryLimits } : {}),
visibleAfterIso: input.visibleAfterIso, visibleAfterIso: input.visibleAfterIso,
})) }))
) { ) {
@@ -226,6 +226,7 @@ export async function listWorkOrders({
categoryId = 0, categoryId = 0,
workerSharingId = 0, workerSharingId = 0,
hallCandidateLimit = 0, hallCandidateLimit = 0,
hallCategoryLimits = {},
visibleAfterIso = '', visibleAfterIso = '',
excludeFilledSharing = false, excludeFilledSharing = false,
vipEvidencePending = false, vipEvidencePending = false,
@@ -247,6 +248,7 @@ export async function listWorkOrders({
categoryId, categoryId,
workerSharingId, workerSharingId,
hallCandidateLimit, hallCandidateLimit,
hallCategoryLimits,
visibleAfterIso, visibleAfterIso,
excludeFilledSharing, excludeFilledSharing,
vipEvidencePending, vipEvidencePending,
@@ -652,6 +654,7 @@ export function buildWorkOrderWhere({
categoryId = 0, categoryId = 0,
workerSharingId = 0, workerSharingId = 0,
hallCandidateLimit = 0, hallCandidateLimit = 0,
hallCategoryLimits = {},
visibleAfterIso = '', visibleAfterIso = '',
excludeFilledSharing = false, excludeFilledSharing = false,
vipEvidencePending = false, vipEvidencePending = false,
@@ -843,7 +846,9 @@ export function buildWorkOrderWhere({
} }
if (hallCandidateLimit > 0) { if (hallCandidateLimit > 0) {
params.push(hallCandidateLimit) 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) { if (visibleAfterIso) {
params.push(visibleAfterIso) params.push(visibleAfterIso)
@@ -863,23 +868,34 @@ export function buildWorkOrderWhere({
} }
/** 大厅容量先选出全局队首,分类、关键词和 VIP 延迟只能在该集合内继续筛选。 */ /** 大厅容量先选出全局队首,分类、关键词和 VIP 延迟只能在该集合内继续筛选。 */
function buildHallCandidateIdsSql(limitPlaceholder: string) { function buildHallCandidateIdsSql(limitPlaceholder: string, categoryLimitsPlaceholder: string) {
return ` return `
SELECT candidate.id SELECT ranked.id
FROM work_orders candidate FROM (
LEFT JOIN ( SELECT candidate.id, candidate.category_id, candidate.hall_queued_at,
SELECT ROW_NUMBER() OVER (
work_order_id, PARTITION BY candidate.category_id
SUM(quantity) FILTER (WHERE status != 'cancelled') AS active_share_quantity ORDER BY candidate.hall_queued_at ASC NULLS LAST, candidate.id ASC
FROM work_order_shares ) AS category_rank
GROUP BY work_order_id FROM work_orders candidate
) candidate_shares ON candidate_shares.work_order_id = candidate.id LEFT JOIN (
WHERE candidate.status = 'open' SELECT
AND ( work_order_id,
candidate.sharing_enabled IS NOT TRUE SUM(quantity) FILTER (WHERE status != 'cancelled') AS active_share_quantity
OR COALESCE(candidate_shares.active_share_quantity, 0) < candidate.sharing_total_quantity FROM work_order_shares
) GROUP BY work_order_id
ORDER BY candidate.hall_queued_at ASC NULLS LAST, candidate.id ASC ) 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} LIMIT ${limitPlaceholder}
` `
} }
@@ -887,7 +903,12 @@ function buildHallCandidateIdsSql(limitPlaceholder: string) {
/** 抢单操作在事务中复核容量与当前打手的可见延迟,避免通过订单 ID 绕过大厅队列。 */ /** 抢单操作在事务中复核容量与当前打手的可见延迟,避免通过订单 ID 绕过大厅队列。 */
export async function isWorkOrderWithinHallCapacityWithClient( export async function isWorkOrderWithinHallCapacityWithClient(
client: PoolClient, 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 }>( const result = await client.query<{ visible: boolean }>(
` `
@@ -895,13 +916,14 @@ export async function isWorkOrderWithinHallCapacityWithClient(
SELECT 1 SELECT 1
FROM work_orders wo FROM work_orders wo
WHERE wo.id = $1 WHERE wo.id = $1
AND wo.id IN (${buildHallCandidateIdsSql('$2')}) AND wo.id IN (${buildHallCandidateIdsSql('$2', '$3')})
AND ($3::timestamptz IS NULL OR wo.hall_queued_at IS NULL OR wo.hall_queued_at <= $3) AND ($4::timestamptz IS NULL OR wo.hall_queued_at IS NULL OR wo.hall_queued_at <= $4)
) AS visible ) AS visible
`, `,
[ [
input.workOrderId, input.workOrderId,
input.hallCandidateLimit, input.hallCandidateLimit,
JSON.stringify(input.hallCategoryLimits || {}),
input.visibleAfterIso ? input.visibleAfterIso : null, input.visibleAfterIso ? input.visibleAfterIso : null,
], ],
) )
@@ -103,6 +103,7 @@ export async function joinWorkOrderShare(input: {
takeRemaining?: boolean takeRemaining?: boolean
maxActiveOrders: number maxActiveOrders: number
hallCandidateLimit?: number hallCandidateLimit?: number
hallCategoryLimits?: Record<string, number>
visibleAfterIso?: string visibleAfterIso?: string
now: string now: string
}): Promise<{ }): Promise<{
@@ -137,6 +138,7 @@ export async function joinWorkOrderShare(input: {
!(await isWorkOrderWithinHallCapacityWithClient(client, { !(await isWorkOrderWithinHallCapacityWithClient(client, {
workOrderId: input.workOrderId, workOrderId: input.workOrderId,
hallCandidateLimit: input.hallCandidateLimit, hallCandidateLimit: input.hallCandidateLimit,
...(input.hallCategoryLimits ? { hallCategoryLimits: input.hallCategoryLimits } : {}),
visibleAfterIso: input.visibleAfterIso, visibleAfterIso: input.visibleAfterIso,
})) }))
) { ) {
@@ -6,6 +6,7 @@ import {
import { import {
createWorkOrder, createWorkOrder,
createWorkOrderEvent, createWorkOrderEvent,
getWorkCategoryById,
listWorkOrdersByPlatformOrderId, listWorkOrdersByPlatformOrderId,
} from '../../repositories/worker-platform/index.js' } from '../../repositories/worker-platform/index.js'
import type { JsonObject } from '../../types/json.js' import type { JsonObject } from '../../types/json.js'
@@ -47,6 +48,18 @@ export async function createAdminMockWorkOrder(payload: JsonObject = {}) {
: 0 : 0
: normalizeAmountFen(payload.requiredDepositAmount, 0) : normalizeAmountFen(payload.requiredDepositAmount, 0)
const paymentFen = input.paymentAmount const paymentFen = input.paymentAmount
const requestedCategoryId = Number(payload.categoryId || payload.category_id || 0)
let categoryId = defaults.category?.id || null
if (requestedCategoryId > 0) {
const category = await getWorkCategoryById(requestedCategoryId)
if (!category || category.status !== 'active') {
throw createHttpError('所选接单分类不存在或已停用', {
statusCode: 409,
errorCode: 'mock_work_order_category_invalid',
})
}
categoryId = Number(category.id)
}
const materialComplete = payload.materialComplete === true || payload.materialComplete === 'true' const materialComplete = payload.materialComplete === true || payload.materialComplete === 'true'
const fields = normalizeRequirementFields(payload.fields) const fields = normalizeRequirementFields(payload.fields)
const { platformOrderId, productName, shop } = input const { platformOrderId, productName, shop } = input
@@ -92,7 +105,7 @@ export async function createAdminMockWorkOrder(payload: JsonObject = {}) {
workOrderNo: randomId('WO'), workOrderNo: randomId('WO'),
platformOrderId, platformOrderId,
productName, productName,
categoryId: defaults.category?.id || null, categoryId,
status: materialComplete ? WORK_ORDER_STATUS.UNASSIGNED : WORK_ORDER_STATUS.PENDING_MATERIAL, status: materialComplete ? WORK_ORDER_STATUS.UNASSIGNED : WORK_ORDER_STATUS.PENDING_MATERIAL,
rewardAmount, rewardAmount,
requiredDepositAmount, requiredDepositAmount,
@@ -250,6 +250,7 @@ export async function publishAdminWorkOrder(workOrderId: number | string, actorN
workOrderId: Number(workOrder.id), workOrderId: Number(workOrder.id),
statuses: [WORK_ORDER_STATUS.OPEN], statuses: [WORK_ORDER_STATUS.OPEN],
hallCandidateLimit: hallConfig.maxVisibleOrders, hallCandidateLimit: hallConfig.maxVisibleOrders,
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
excludeFilledSharing: true, excludeFilledSharing: true,
}) })
return { return {
@@ -30,6 +30,7 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
const page = normalizePage(query.page) const page = normalizePage(query.page)
const pageSize = normalizePageSize(query.pageSize) const pageSize = normalizePageSize(query.pageSize)
const workOrderId = normalizeOptionalId(query.workOrderId ?? query.work_order_id) || 0 const workOrderId = normalizeOptionalId(query.workOrderId ?? query.work_order_id) || 0
const categoryId = normalizeOptionalId(query.categoryId ?? query.category_id) || 0
const workerId = normalizeOptionalId(query.workerId ?? query.worker_id) || 0 const workerId = normalizeOptionalId(query.workerId ?? query.worker_id) || 0
const workerLevelIds = normalizeStatuses(query.workerLevelIds ?? query.worker_level_ids) const workerLevelIds = normalizeStatuses(query.workerLevelIds ?? query.worker_level_ids)
.map(normalizeOptionalId) .map(normalizeOptionalId)
@@ -43,6 +44,7 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
const filters = { const filters = {
statuses, statuses,
workOrderId, workOrderId,
categoryId,
keyword: String(query.keyword || '').trim(), keyword: String(query.keyword || '').trim(),
workerSharingId: workerId, workerSharingId: workerId,
workerLevelIds, workerLevelIds,
@@ -5,9 +5,44 @@ import { getWorkerHallConfig, saveWorkerHallConfig } from './worker-hall-config-
export async function getAdminWorkerPlatformSummary() { export async function getAdminWorkerPlatformSummary() {
const hallConfig = getWorkerHallConfig() const hallConfig = getWorkerHallConfig()
const [pendingWorkers, pendingMaterial, openOrders, inProgressOrders] = await Promise.all([ const [pendingWorkers, pendingMaterial, openOrders, visibleOpenOrders, inProgressOrders] =
listWorkerUsers({ page: 1, pageSize: 1, status: 'pending_review' }), await Promise.all([
listWorkOrders({ page: 1, pageSize: 1, statuses: [WORK_ORDER_STATUS.PENDING_MATERIAL] }), 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,
}),
])
return {
pendingWorkerCount: pendingWorkers.total,
pendingMaterialCount: pendingMaterial.total,
openOrderCount: visibleOpenOrders.total,
queuedHallOrderCount: Math.max(0, openOrders.total - visibleOpenOrders.total),
hallMaxVisibleOrders: hallConfig.maxVisibleOrders,
inProgressOrderCount: inProgressOrders.total,
}
}
export async function getAdminWorkerHallConfig() {
const config = getWorkerHallConfig()
const [openOrders, visibleOpenOrders] = await Promise.all([
listWorkOrders({ listWorkOrders({
page: 1, page: 1,
pageSize: 1, pageSize: 1,
@@ -17,32 +52,16 @@ export async function getAdminWorkerPlatformSummary() {
listWorkOrders({ listWorkOrders({
page: 1, page: 1,
pageSize: 1, pageSize: 1,
statuses: [WORK_ORDER_STATUS.IN_PROGRESS], statuses: [WORK_ORDER_STATUS.OPEN],
useOperationalStatus: true, hallCandidateLimit: config.maxVisibleOrders,
hallCategoryLimits: config.categoryMaxVisibleOrders,
excludeFilledSharing: true,
}), }),
]) ])
return {
pendingWorkerCount: pendingWorkers.total,
pendingMaterialCount: pendingMaterial.total,
openOrderCount: Math.min(openOrders.total, hallConfig.maxVisibleOrders),
queuedHallOrderCount: Math.max(0, openOrders.total - hallConfig.maxVisibleOrders),
hallMaxVisibleOrders: hallConfig.maxVisibleOrders,
inProgressOrderCount: inProgressOrders.total,
}
}
export async function getAdminWorkerHallConfig() {
const config = getWorkerHallConfig()
const openOrders = await listWorkOrders({
page: 1,
pageSize: 1,
statuses: [WORK_ORDER_STATUS.OPEN],
excludeFilledSharing: true,
})
return { return {
...config, ...config,
visibleOrderCount: Math.min(openOrders.total, config.maxVisibleOrders), visibleOrderCount: visibleOpenOrders.total,
queuedOrderCount: Math.max(0, openOrders.total - config.maxVisibleOrders), queuedOrderCount: Math.max(0, openOrders.total - visibleOpenOrders.total),
} }
} }
@@ -55,6 +55,7 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session:
takeRemaining: true, takeRemaining: true,
maxActiveOrders: permissions.maxActiveOrders, maxActiveOrders: permissions.maxActiveOrders,
hallCandidateLimit: hallConfig.maxVisibleOrders, hallCandidateLimit: hallConfig.maxVisibleOrders,
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
visibleAfterIso, visibleAfterIso,
now, now,
}) })
@@ -91,6 +92,7 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session:
depositAmount: freezeAmount, depositAmount: freezeAmount,
maxActiveOrders: permissions.maxActiveOrders, maxActiveOrders: permissions.maxActiveOrders,
hallCandidateLimit: hallConfig.maxVisibleOrders, hallCandidateLimit: hallConfig.maxVisibleOrders,
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
visibleAfterIso, visibleAfterIso,
deadlineAt: resolveWorkOrderDeadlineAt(workOrder, now), deadlineAt: resolveWorkOrderDeadlineAt(workOrder, now),
now, now,
@@ -143,6 +145,7 @@ export async function joinWorkerSharingOrder(
quantity, quantity,
maxActiveOrders: permissions.maxActiveOrders, maxActiveOrders: permissions.maxActiveOrders,
hallCandidateLimit: hallConfig.maxVisibleOrders, hallCandidateLimit: hallConfig.maxVisibleOrders,
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
visibleAfterIso, visibleAfterIso,
now, now,
}) })
@@ -7,17 +7,36 @@ import {
} from './worker-hall-config-service.js' } from './worker-hall-config-service.js'
test('大厅配置默认展示 50 个订单', () => { test('大厅配置默认展示 50 个订单', () => {
assert.deepEqual(createDefaultWorkerHallConfig(), { maxVisibleOrders: 50 }) assert.deepEqual(createDefaultWorkerHallConfig(), {
assert.deepEqual(normalizeWorkerHallConfig({}), { maxVisibleOrders: 50 }) maxVisibleOrders: 50,
categoryMaxVisibleOrders: {},
})
assert.deepEqual(normalizeWorkerHallConfig({}), {
maxVisibleOrders: 50,
categoryMaxVisibleOrders: {},
})
}) })
test('大厅配置只接受 1 到 500 的整数', () => { test('大厅配置只接受 1 到 500 的整数', () => {
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 88 }), { maxVisibleOrders: 88 }) assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 88 }), {
maxVisibleOrders: 88,
categoryMaxVisibleOrders: {},
})
assert.deepEqual(normalizeWorkerHallConfig({ max_visible_orders: 999 }), { assert.deepEqual(normalizeWorkerHallConfig({ max_visible_orders: 999 }), {
maxVisibleOrders: 500, maxVisibleOrders: 500,
categoryMaxVisibleOrders: {},
})
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 0 }), {
maxVisibleOrders: 1,
categoryMaxVisibleOrders: {},
}) })
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 0 }), { maxVisibleOrders: 1 })
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 1.5 }), { assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 1.5 }), {
maxVisibleOrders: 50, maxVisibleOrders: 50,
categoryMaxVisibleOrders: {},
}) })
assert.deepEqual(
normalizeWorkerHallConfig({ categoryMaxVisibleOrders: { '1': 12, '2': 0, '3': 501 } })
.categoryMaxVisibleOrders,
{ '1': 12 },
)
}) })
@@ -4,6 +4,7 @@ import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-sto
export type WorkerHallConfig = { export type WorkerHallConfig = {
maxVisibleOrders: number maxVisibleOrders: number
categoryMaxVisibleOrders: Record<string, number>
} }
export function getWorkerHallConfig(): WorkerHallConfig { export function getWorkerHallConfig(): WorkerHallConfig {
@@ -23,7 +24,7 @@ export async function saveWorkerHallConfig(rawValue: unknown): Promise<WorkerHal
} }
export function createDefaultWorkerHallConfig(): WorkerHallConfig { export function createDefaultWorkerHallConfig(): WorkerHallConfig {
return { maxVisibleOrders: 50 } return { maxVisibleOrders: 50, categoryMaxVisibleOrders: {} }
} }
export function normalizeWorkerHallConfig(rawValue: unknown): WorkerHallConfig { export function normalizeWorkerHallConfig(rawValue: unknown): WorkerHallConfig {
@@ -31,9 +32,24 @@ export function normalizeWorkerHallConfig(rawValue: unknown): WorkerHallConfig {
const value = Number(source.maxVisibleOrders ?? source.max_visible_orders) const value = Number(source.maxVisibleOrders ?? source.max_visible_orders)
return { return {
maxVisibleOrders: Number.isInteger(value) ? Math.min(500, Math.max(1, value)) : 50, maxVisibleOrders: Number.isInteger(value) ? Math.min(500, Math.max(1, value)) : 50,
categoryMaxVisibleOrders: normalizeCategoryMaxVisibleOrders(
source.categoryMaxVisibleOrders ?? source.category_max_visible_orders,
),
} }
} }
function normalizeCategoryMaxVisibleOrders(value: unknown): Record<string, number> {
if (!isPlainObject(value)) return {}
return Object.entries(value).reduce<Record<string, number>>((result, [categoryId, raw]) => {
const id = Number(categoryId)
const limit = Number(raw)
if (Number.isInteger(id) && id > 0 && Number.isInteger(limit) && limit >= 1 && limit <= 500) {
result[String(id)] = limit
}
return result
}, {})
}
function isPlainObject(value: unknown): value is JsonObject { function isPlainObject(value: unknown): value is JsonObject {
return Object.prototype.toString.call(value) === '[object Object]' return Object.prototype.toString.call(value) === '[object Object]'
} }
@@ -39,6 +39,7 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work
keyword: String(query.keyword || '').trim(), keyword: String(query.keyword || '').trim(),
categoryId: categoryId || 0, categoryId: categoryId || 0,
hallCandidateLimit: hallConfig.maxVisibleOrders, hallCandidateLimit: hallConfig.maxVisibleOrders,
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
visibleAfterIso, visibleAfterIso,
excludeFilledSharing: true, excludeFilledSharing: true,
pinnedFirst: true, pinnedFirst: true,
@@ -1,11 +1,17 @@
import { useQuery, useQueryClient } from '@tanstack/react-query' import { useQuery, useQueryClient } from '@tanstack/react-query'
import { App, Button, Card, Descriptions, Form, InputNumber, Space } from 'antd' import { App, Button, Card, Descriptions, Form, InputNumber, Space, Table, Tag } from 'antd'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { fetchAdminWorkerHallConfig, saveAdminWorkerHallConfig } from '@/services/admin' import {
fetchAdminWorkCategories,
fetchAdminWorkerHallConfig,
saveAdminWorkerHallConfig,
} from '@/services/admin'
import type { WorkCategory } from '@/types/worker-platform'
type HallConfigFormValues = { type HallConfigFormValues = {
maxVisibleOrders: number maxVisibleOrders: number
categoryMaxVisibleOrders: Record<string, number>
} }
export default function HallConfigPanel() { export default function HallConfigPanel() {
@@ -20,13 +26,25 @@ export default function HallConfigPanel() {
const config = configQuery.data?.data const config = configQuery.data?.data
useEffect(() => { useEffect(() => {
if (config) form.setFieldsValue({ maxVisibleOrders: config.maxVisibleOrders }) if (config) {
form.setFieldsValue({
maxVisibleOrders: config.maxVisibleOrders,
categoryMaxVisibleOrders: config.categoryMaxVisibleOrders || {},
})
}
}, [config, form]) }, [config, form])
const categoriesQuery = useQuery({
queryKey: ['admin-worker-platform-categories'],
queryFn: fetchAdminWorkCategories,
})
async function saveConfig(values: HallConfigFormValues) { async function saveConfig(values: HallConfigFormValues) {
setSaving(true) setSaving(true)
try { try {
await saveAdminWorkerHallConfig({ maxVisibleOrders: Number(values.maxVisibleOrders) }) await saveAdminWorkerHallConfig({
maxVisibleOrders: Number(values.maxVisibleOrders),
categoryMaxVisibleOrders: values.categoryMaxVisibleOrders || {},
})
await Promise.all([ await Promise.all([
queryClient.invalidateQueries({ queryKey: ['admin-worker-hall-config'] }), queryClient.invalidateQueries({ queryKey: ['admin-worker-hall-config'] }),
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-summary'] }), queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-summary'] }),
@@ -47,7 +65,7 @@ export default function HallConfigPanel() {
<Descriptions.Item label="等待入厅">{config?.queuedOrderCount || 0}</Descriptions.Item> <Descriptions.Item label="等待入厅">{config?.queuedOrderCount || 0}</Descriptions.Item>
<Descriptions.Item label="当前上限">{config?.maxVisibleOrders || '-'}</Descriptions.Item> <Descriptions.Item label="当前上限">{config?.maxVisibleOrders || '-'}</Descriptions.Item>
</Descriptions> </Descriptions>
<Form form={form} layout="inline" onFinish={saveConfig}> <Form form={form} layout="vertical" onFinish={saveConfig}>
<Form.Item <Form.Item
name="maxVisibleOrders" name="maxVisibleOrders"
label="大厅订单总数" label="大厅订单总数"
@@ -60,6 +78,41 @@ export default function HallConfigPanel() {
</Button> </Button>
</Form.Item> </Form.Item>
<div>
<Tag color="blue"></Tag>
<span style={{ marginLeft: 8, color: '#666' }}>
沿
</span>
</div>
<Table<WorkCategory>
rowKey="categoryId"
size="small"
pagination={false}
loading={categoriesQuery.isLoading}
dataSource={categoriesQuery.data?.data.items || []}
columns={[
{ title: '分类', dataIndex: 'name' },
{
title: '状态',
render: (_, category) => (
<Tag color={category.status === 'active' ? 'green' : 'default'}>
{category.status === 'active' ? '启用' : '停用'}
</Tag>
),
},
{
title: '最多展示',
render: (_, category) => (
<Form.Item
name={['categoryMaxVisibleOrders', String(category.categoryId)]}
style={{ margin: 0 }}
>
<InputNumber min={1} max={500} precision={0} placeholder="跟随总上限" />
</Form.Item>
),
},
]}
/>
</Form> </Form>
</Space> </Space>
</Card> </Card>
@@ -1,9 +1,13 @@
import { PlusOutlined } from '@ant-design/icons' import { PlusOutlined } from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query' import { useQuery, useQueryClient } from '@tanstack/react-query'
import { App, AutoComplete, Button, Card, Form, Input, InputNumber, Select, Switch } from 'antd' import { App, AutoComplete, Button, Card, Form, Input, InputNumber, Select, Switch } from 'antd'
import { useState } from 'react' import { useEffect, useState } from 'react'
import { createAdminMockWorkOrder, fetchAdminMockCustomShops } from '@/services/admin' import {
createAdminMockWorkOrder,
fetchAdminMockCustomShops,
fetchAdminWorkCategories,
} from '@/services/admin'
import { fetchAdminKuaishouIndustryShops } from '@/services/admin/kuaishou-industry' import { fetchAdminKuaishouIndustryShops } from '@/services/admin/kuaishou-industry'
import type { WorkOrderStatistics } from '@/types/worker-platform' import type { WorkOrderStatistics } from '@/types/worker-platform'
import { buildMockWorkOrderShopOptions } from '@/utils/mock-work-order' import { buildMockWorkOrderShopOptions } from '@/utils/mock-work-order'
@@ -69,6 +73,19 @@ export function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise<void
queryKey: ['admin-mock-custom-shops'], queryKey: ['admin-mock-custom-shops'],
queryFn: fetchAdminMockCustomShops, queryFn: fetchAdminMockCustomShops,
}) })
const categoriesQuery = useQuery({
queryKey: ['admin-worker-platform-categories'],
queryFn: fetchAdminWorkCategories,
})
const activeCategories = (categoriesQuery.data?.data.items || []).filter(
(category) => category.status === 'active',
)
const defaultCategoryId = activeCategories[0]?.categoryId
useEffect(() => {
if (!form.getFieldValue('categoryId') && defaultCategoryId) {
form.setFieldValue('categoryId', defaultCategoryId)
}
}, [defaultCategoryId, form])
const shopOptions = [ const shopOptions = [
...buildMockWorkOrderShopOptions(shopsQuery.data?.data.shops || []), ...buildMockWorkOrderShopOptions(shopsQuery.data?.data.shops || []),
...(customShopsQuery.data?.data.items || []).map((name) => ({ ...(customShopsQuery.data?.data.items || []).map((name) => ({
@@ -81,6 +98,7 @@ export function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise<void
platformOrderId: string platformOrderId: string
sellerId: string sellerId: string
productName: string productName: string
categoryId?: number
rewardAmount: number rewardAmount: number
paymentAmount: number paymentAmount: number
materialComplete?: boolean materialComplete?: boolean
@@ -91,9 +109,11 @@ export function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise<void
await createAdminMockWorkOrder({ await createAdminMockWorkOrder({
...values, ...values,
acceptanceMode: values.acceptanceMode || 'standard', acceptanceMode: values.acceptanceMode || 'standard',
categoryId: values.categoryId,
}) })
message.success('Mock 工单已生成') message.success('Mock 工单已生成')
form.resetFields() form.resetFields()
if (defaultCategoryId) form.setFieldValue('categoryId', defaultCategoryId)
// 自定义店铺由后端记忆,刷新后即可在下拉中选到。 // 自定义店铺由后端记忆,刷新后即可在下拉中选到。
await queryClient.invalidateQueries({ queryKey: ['admin-mock-custom-shops'] }) await queryClient.invalidateQueries({ queryKey: ['admin-mock-custom-shops'] })
await onCreated() await onCreated()
@@ -114,6 +134,22 @@ export function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise<void
> >
<Input placeholder="请输入订单号" /> <Input placeholder="请输入订单号" />
</Form.Item> </Form.Item>
<Form.Item
className="worker-order-mock-field-category"
label="分类"
name="categoryId"
rules={[{ required: true, message: '请选择接单分类' }]}
>
<Select
allowClear
loading={categoriesQuery.isLoading}
options={activeCategories.map((category) => ({
value: category.categoryId,
label: category.name,
}))}
placeholder="选择接单分类"
/>
</Form.Item>
<Form.Item <Form.Item
className="worker-order-mock-field-shop" className="worker-order-mock-field-shop"
label="店铺" label="店铺"
@@ -203,6 +203,7 @@ export default function WorkOrdersPanel() {
const [createdRange, setCreatedRange] = useState<[string, string] | null>(null) const [createdRange, setCreatedRange] = useState<[string, string] | null>(null)
const [keywordInput, setKeywordInput] = useState('') const [keywordInput, setKeywordInput] = useState('')
const [workerId, setWorkerId] = useState<number | undefined>() const [workerId, setWorkerId] = useState<number | undefined>()
const [categoryId, setCategoryId] = useState<number | undefined>()
const [workerLevelIds, setWorkerLevelIds] = useState<number[]>([]) const [workerLevelIds, setWorkerLevelIds] = useState<number[]>([])
const [acceptanceMode, setAcceptanceMode] = useState<string | undefined>() const [acceptanceMode, setAcceptanceMode] = useState<string | undefined>()
const [giftPhase, setGiftPhase] = useState<string | undefined>() const [giftPhase, setGiftPhase] = useState<string | undefined>()
@@ -279,6 +280,7 @@ export default function WorkOrdersPanel() {
statuses.join(','), statuses.join(','),
keyword, keyword,
workerId, workerId,
categoryId,
workerLevelIds.join(','), workerLevelIds.join(','),
targetWorkOrderId, targetWorkOrderId,
acceptanceMode || '', acceptanceMode || '',
@@ -296,6 +298,7 @@ export default function WorkOrdersPanel() {
acceptanceMode: acceptanceMode || undefined, acceptanceMode: acceptanceMode || undefined,
giftPhase: giftPhase || undefined, giftPhase: giftPhase || undefined,
workOrderId: targetWorkOrderId, workOrderId: targetWorkOrderId,
categoryId,
keyword, keyword,
workerId, workerId,
workerLevelIds: workerLevelIds:
@@ -346,6 +349,7 @@ export default function WorkOrdersPanel() {
statuses, statuses,
keyword, keyword,
workerId, workerId,
categoryId,
workerLevelIds, workerLevelIds,
targetWorkOrderId, targetWorkOrderId,
acceptanceMode, acceptanceMode,
@@ -671,6 +675,7 @@ export default function WorkOrdersPanel() {
setKeyword('') setKeyword('')
setStatuses([]) setStatuses([])
setWorkerId(undefined) setWorkerId(undefined)
setCategoryId(undefined)
setWorkerLevelIds([]) setWorkerLevelIds([])
setAcceptanceMode(undefined) setAcceptanceMode(undefined)
setGiftPhase(undefined) setGiftPhase(undefined)
@@ -1240,6 +1245,23 @@ export default function WorkOrdersPanel() {
setPage(1) setPage(1)
}} }}
/> />
<Select
allowClear
showSearch
optionFilterProp="label"
value={categoryId}
placeholder="全部接单分类"
style={{ width: 180 }}
loading={categoriesQuery.isLoading}
options={(categoriesQuery.data?.data.items || []).map((category) => ({
value: category.categoryId,
label: category.name,
}))}
onChange={(nextCategoryId) => {
setCategoryId(nextCategoryId)
setPage(1)
}}
/>
{showWorkerLevelFilter ? ( {showWorkerLevelFilter ? (
<Select <Select
mode="multiple" mode="multiple"
@@ -85,7 +85,9 @@ export function fetchAdminWorkerHallConfig() {
return apiGet<WorkerHallConfig>('/api/v1/admin/worker-platform/hall-config') return apiGet<WorkerHallConfig>('/api/v1/admin/worker-platform/hall-config')
} }
export function saveAdminWorkerHallConfig(payload: Pick<WorkerHallConfig, 'maxVisibleOrders'>) { export function saveAdminWorkerHallConfig(
payload: Pick<WorkerHallConfig, 'maxVisibleOrders' | 'categoryMaxVisibleOrders'>,
) {
return apiPost<WorkerHallConfig>('/api/v1/admin/worker-platform/hall-config', payload) return apiPost<WorkerHallConfig>('/api/v1/admin/worker-platform/hall-config', payload)
} }
@@ -574,6 +576,7 @@ export function createAdminMockWorkOrder(payload: {
platformOrderId: string platformOrderId: string
sellerId: string sellerId: string
productName: string productName: string
categoryId?: number
rewardAmount: number rewardAmount: number
paymentAmount: number paymentAmount: number
materialComplete?: boolean materialComplete?: boolean
+6 -1
View File
@@ -281,6 +281,7 @@ export type WorkerAnnouncementConfig = {
export type WorkerHallConfig = { export type WorkerHallConfig = {
maxVisibleOrders: number maxVisibleOrders: number
categoryMaxVisibleOrders: Record<string, number>
visibleOrderCount: number visibleOrderCount: number
queuedOrderCount: number queuedOrderCount: number
} }
@@ -494,7 +495,11 @@ export type WorkOrderShare = {
} }
export type WorkOrderGiftPhase = export type WorkOrderGiftPhase =
'material_required' | 'material_submitted' | 'friend_countdown' | 'gift_ready' | string | 'material_required'
| 'material_submitted'
| 'friend_countdown'
| 'gift_ready'
| string
/** 好友赠送验收模式的打手本人资料 */ /** 好友赠送验收模式的打手本人资料 */
export type BoosterMaterial = { export type BoosterMaterial = {