新增大厅订单容量控制

This commit is contained in:
yml2213
2026-08-21 14:02:30 +08:00
parent cc03382921
commit 874b093a4b
20 changed files with 407 additions and 44 deletions
@@ -169,6 +169,16 @@ export function publishWorkOrderRealtimeChange(input: {
})
}
/** 大厅全局配置变化时刷新所有打手的候选集合。 */
export function publishWorkerHallRealtimeRefresh() {
publishRealtimeEvent({
type: 'work_order.changed',
scopes: ['worker_hall'],
operation: 'refresh',
broadcastWorkers: true,
})
}
/** 资金申请变更会同步后台资金页与申请所属打手的钱包页面。 */
export function publishWorkerFinanceRealtimeChange(input: {
requestId: number
@@ -282,17 +292,13 @@ async function publishWorkOrderSnapshots(input: {
})
if (input.hallChanged) {
const sharingFilled = isWorkOrderSharingFilled(
workOrder.sharing_enabled === true,
Number(workOrder.sharing_total_quantity || 1),
joinedQuantity,
)
publishRealtimeEvent({
type: 'work_order.changed',
entityId: workOrderId,
scopes: ['worker_hall'],
operation: workOrder.status === 'open' && !sharingFilled ? 'upsert' : 'remove',
data: mapWorkerHallWorkOrder(workOrder, shares),
// 队首补位可能影响多张订单,客户端需要重新读取大厅快照。
operation: 'refresh',
data: { workOrderId },
broadcastWorkers: true,
})
}
@@ -329,24 +335,6 @@ async function publishWorkOrderSnapshots(input: {
}
}
function mapWorkerHallWorkOrder(
workOrder: NonNullable<Awaited<ReturnType<typeof getWorkOrderById>>>,
shares: Awaited<ReturnType<typeof listWorkOrderShares>>,
) {
const mapped = mapWorkOrderAdmin(workOrder)
const { worker: _worker, orderId: _orderId, orderItemId: _orderItemId, ...visibleOrder } = mapped
const joinedQuantity = shares
.filter((share) => share.status !== 'cancelled')
.reduce((sum, share) => sum + Number(share.quantity || 0), 0)
const pendingSubmissionCount = shares.filter((share) => share.status === 'joined').length
return {
...visibleOrder,
// 大厅接收端根据自身等级重新计算实际冻结金额,避免不同等级看到同一笔错误押金。
requiredDepositAmount: mapped.requiredDepositAmount,
sharingProgress: { joinedQuantity, pendingSubmissionCount },
}
}
async function publishWorkerFinanceSnapshots(input: {
requestId: number
workerId: number
@@ -91,6 +91,7 @@ import {
getWorkerAnnouncementConfig,
saveWorkerAnnouncementConfig,
} from './worker-announcement-config-service.js'
import { getWorkerHallConfig, saveWorkerHallConfig } from './worker-hall-config-service.js'
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
import {
createWorkOrderMaterialAdminNotification,
@@ -98,6 +99,7 @@ import {
} from '../admin/admin-notification-service.js'
import {
publishWorkerFinanceRealtimeChange,
publishWorkerHallRealtimeRefresh,
publishWorkerWalletRealtimeChange,
publishWorkOrderRealtimeChange,
} from '../realtime/realtime-event-service.js'
@@ -2235,6 +2237,7 @@ export async function publishAdminWorkOrder(workOrderId: number | string, actorN
const updated = await updateWorkOrder(workOrder.id, {
status: WORK_ORDER_STATUS.OPEN,
published_at: now,
hall_queued_at: now,
updated_at: now,
})
if (!updated) {
@@ -2260,7 +2263,20 @@ export async function publishAdminWorkOrder(workOrderId: number | string, actorN
now,
})
publishWorkOrderRealtimeChange({ workOrderId: Number(workOrder.id), hallChanged: true })
return { order: mapWorkOrderAdmin(updated), voucherConsume }
const hallConfig = getWorkerHallConfig()
const hallEntry = await listWorkOrders({
page: 1,
pageSize: 1,
workOrderId: Number(workOrder.id),
statuses: [WORK_ORDER_STATUS.OPEN],
hallCandidateLimit: hallConfig.maxVisibleOrders,
excludeFilledSharing: true,
})
return {
order: mapWorkOrderAdmin(updated),
voucherConsume,
hallVisible: hallEntry.total > 0,
}
}
export async function unpublishAdminWorkOrder(workOrderId: number | string, actorName = '') {
@@ -2698,6 +2714,7 @@ async function resolveWorkOrderRealtimeWorkerIds(workOrder: WorkOrderRow) {
}
export async function getAdminWorkerPlatformSummary() {
const hallConfig = getWorkerHallConfig()
const [pendingWorkers, pendingMaterial, openOrders, inProgressOrders] = await Promise.all([
listWorkerUsers({ page: 1, pageSize: 1, status: 'pending_review' }),
listWorkOrders({ page: 1, pageSize: 1, statuses: [WORK_ORDER_STATUS.PENDING_MATERIAL] }),
@@ -2717,11 +2734,35 @@ export async function getAdminWorkerPlatformSummary() {
return {
pendingWorkerCount: pendingWorkers.total,
pendingMaterialCount: pendingMaterial.total,
openOrderCount: openOrders.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 {
...config,
visibleOrderCount: Math.min(openOrders.total, config.maxVisibleOrders),
queuedOrderCount: Math.max(0, openOrders.total - config.maxVisibleOrders),
}
}
export async function saveAdminWorkerHallConfig(rawValue: unknown) {
await saveWorkerHallConfig(rawValue)
const config = await getAdminWorkerHallConfig()
publishWorkerHallRealtimeRefresh()
return config
}
export async function syncWorkerOrdersForSourceOrder(
order: OrderRow,
orderItems: OrderItemRow[],
@@ -3,3 +3,4 @@ export * from './worker-service.js'
export * from './admin-service.js'
export * from './worker-product-match-config-service.js'
export * from './worker-announcement-config-service.js'
export * from './worker-hall-config-service.js'
@@ -34,6 +34,7 @@ function buildWorkOrder(overrides: Partial<WorkOrderRow> = {}): WorkOrderRow {
sharing_total_quantity: 1,
sharing_unit_reward: 0,
published_at: null,
hall_queued_at: null,
pinned_at: null,
assigned_at: null,
submitted_at: null,
@@ -0,0 +1,23 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
createDefaultWorkerHallConfig,
normalizeWorkerHallConfig,
} from './worker-hall-config-service.js'
test('大厅配置默认展示 50 个订单', () => {
assert.deepEqual(createDefaultWorkerHallConfig(), { maxVisibleOrders: 50 })
assert.deepEqual(normalizeWorkerHallConfig({}), { maxVisibleOrders: 50 })
})
test('大厅配置只接受 1 到 500 的整数', () => {
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 88 }), { maxVisibleOrders: 88 })
assert.deepEqual(normalizeWorkerHallConfig({ max_visible_orders: 999 }), {
maxVisibleOrders: 500,
})
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 0 }), { maxVisibleOrders: 1 })
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 1.5 }), {
maxVisibleOrders: 50,
})
})
@@ -0,0 +1,39 @@
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
import type { JsonObject } from '../../types/json.js'
import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js'
export type WorkerHallConfig = {
maxVisibleOrders: number
}
export function getWorkerHallConfig(): WorkerHallConfig {
return readAppConfigEntry({
configKey: APP_CONFIG_KEYS.workerHall,
fallback: createDefaultWorkerHallConfig,
normalize: normalizeWorkerHallConfig,
})
}
export async function saveWorkerHallConfig(rawValue: unknown): Promise<WorkerHallConfig> {
return saveAppConfigEntry({
configKey: APP_CONFIG_KEYS.workerHall,
value: rawValue,
normalize: normalizeWorkerHallConfig,
})
}
export function createDefaultWorkerHallConfig(): WorkerHallConfig {
return { maxVisibleOrders: 50 }
}
export function normalizeWorkerHallConfig(rawValue: unknown): WorkerHallConfig {
const source = isPlainObject(rawValue) ? rawValue : {}
const value = Number(source.maxVisibleOrders ?? source.max_visible_orders)
return {
maxVisibleOrders: Number.isInteger(value) ? Math.min(500, Math.max(1, value)) : 50,
}
}
function isPlainObject(value: unknown): value is JsonObject {
return Object.prototype.toString.call(value) === '[object Object]'
}
@@ -831,6 +831,7 @@ function buildWorkOrderRow(overrides: Partial<WorkOrderRow> = {}): WorkOrderRow
acceptance_json: '{}',
problem_note: '',
published_at: null,
hall_queued_at: null,
assigned_at: null,
submitted_at: null,
accepted_at: null,
@@ -86,6 +86,7 @@ import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-
import { getSmsProvider } from '../sms/index.js'
import { getWorkerFinanceConfig } from './worker-finance-config-service.js'
import { getWorkerAnnouncementConfig } from './worker-announcement-config-service.js'
import { getWorkerHallConfig } from './worker-hall-config-service.js'
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
import {
createWorkerAcceptanceAdminNotification,
@@ -1470,12 +1471,14 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work
const visibleDelaySeconds = Number(permissions.visibleDelaySeconds || 0)
const visibleAfterIso =
visibleDelaySeconds > 0 ? new Date(Date.now() - visibleDelaySeconds * 1000).toISOString() : ''
const hallConfig = getWorkerHallConfig()
const { items, total } = await listWorkOrders({
page,
pageSize,
statuses: [WORK_ORDER_STATUS.OPEN],
keyword: String(query.keyword || '').trim(),
categoryId: categoryId || 0,
hallCandidateLimit: hallConfig.maxVisibleOrders,
visibleAfterIso,
excludeFilledSharing: true,
pinnedFirst: true,
@@ -1555,6 +1558,11 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session:
requireActiveWorkerSession(session)
const worker = await getRequiredWorker(session.workerId)
const permissions = resolveWorkerPermissions(worker)
const hallConfig = getWorkerHallConfig()
const now = nowIso()
const visibleDelaySeconds = Number(permissions.visibleDelaySeconds || 0)
const visibleAfterIso =
visibleDelaySeconds > 0 ? new Date(Date.now() - visibleDelaySeconds * 1000).toISOString() : ''
const workOrder = await getRequiredWorkOrder(workOrderId)
if (workOrder.status !== WORK_ORDER_STATUS.OPEN || workOrder.assigned_worker_id) {
throw createHttpError('该订单已不可抢', {
@@ -1570,7 +1578,9 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session:
quantity: 0,
takeRemaining: true,
maxActiveOrders: permissions.maxActiveOrders,
now: nowIso(),
hallCandidateLimit: hallConfig.maxVisibleOrders,
visibleAfterIso,
now,
})
if (!result.share) {
throw resolveWorkOrderShareJoinError(result.failureReason)
@@ -1604,8 +1614,10 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session:
workerId: Number(worker.id),
depositAmount: freezeAmount,
maxActiveOrders: permissions.maxActiveOrders,
deadlineAt: resolveWorkOrderDeadlineAt(workOrder, nowIso()),
now: nowIso(),
hallCandidateLimit: hallConfig.maxVisibleOrders,
visibleAfterIso,
deadlineAt: resolveWorkOrderDeadlineAt(workOrder, now),
now,
})
if (!grabbed.order) {
throwGrabWorkOrderFailure(grabbed.failureReason)
@@ -2036,6 +2048,10 @@ export async function joinWorkerSharingOrder(
requireActiveWorkerSession(session)
const worker = await getRequiredWorker(session.workerId)
const permissions = resolveWorkerPermissions(worker)
const hallConfig = getWorkerHallConfig()
const visibleDelaySeconds = Number(permissions.visibleDelaySeconds || 0)
const visibleAfterIso =
visibleDelaySeconds > 0 ? new Date(Date.now() - visibleDelaySeconds * 1000).toISOString() : ''
const quantity = normalizePositiveInteger(payload.quantity, 1)
const now = nowIso()
const result = await joinWorkOrderShare({
@@ -2043,6 +2059,8 @@ export async function joinWorkerSharingOrder(
workerId: Number(worker.id),
quantity,
maxActiveOrders: permissions.maxActiveOrders,
hallCandidateLimit: hallConfig.maxVisibleOrders,
visibleAfterIso,
now,
})
if (!result.share) {