新增大厅订单容量控制
This commit is contained in:
@@ -6,6 +6,7 @@ export const APP_CONFIG_KEYS = {
|
|||||||
workerPlatformNotifications: 'worker_platform_notifications',
|
workerPlatformNotifications: 'worker_platform_notifications',
|
||||||
workerProductMatch: 'worker_product_match',
|
workerProductMatch: 'worker_product_match',
|
||||||
workerAnnouncement: 'worker_announcement',
|
workerAnnouncement: 'worker_announcement',
|
||||||
|
workerHall: 'worker_hall',
|
||||||
cloudtentaclesSources: 'cloudtentacles_sources',
|
cloudtentaclesSources: 'cloudtentacles_sources',
|
||||||
cloudtentaclesSession: 'cloudtentacles_session',
|
cloudtentaclesSession: 'cloudtentacles_session',
|
||||||
cloudtentaclesOverrideRules: 'cloudtentacles_override_rules',
|
cloudtentaclesOverrideRules: 'cloudtentacles_override_rules',
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- 051_worker_hall_queue.sql —— 抢单大厅展示队列与容量查询索引。
|
||||||
|
|
||||||
|
ALTER TABLE work_orders ADD COLUMN IF NOT EXISTS hall_queued_at TIMESTAMPTZ NULL;
|
||||||
|
|
||||||
|
UPDATE work_orders
|
||||||
|
SET hall_queued_at = COALESCE(published_at, updated_at, created_at)
|
||||||
|
WHERE status = 'open'
|
||||||
|
AND hall_queued_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_work_orders_hall_queue
|
||||||
|
ON work_orders(hall_queued_at ASC, id ASC)
|
||||||
|
WHERE status = 'open';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN work_orders.hall_queued_at IS '进入抢单大厅候选队列的时间;用于按先进先出限制大厅展示数量';
|
||||||
@@ -85,3 +85,21 @@ test('未指定打手的运营状态统计使用一次全表拼单汇总', () =>
|
|||||||
)
|
)
|
||||||
assert.doesNotMatch(query.joins, /worker_visible/)
|
assert.doesNotMatch(query.joins, /worker_visible/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('大厅容量先确定全局候选集合,再应用页面筛选条件', () => {
|
||||||
|
const query = buildWorkOrderWhere({
|
||||||
|
statuses: ['open'],
|
||||||
|
keyword: '测试商品',
|
||||||
|
categoryId: 3,
|
||||||
|
hallCandidateLimit: 50,
|
||||||
|
visibleAfterIso: '2026-08-21T00:00:00.000Z',
|
||||||
|
excludeFilledSharing: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.match(query.whereClause, /candidate\.status = 'open'/)
|
||||||
|
assert.match(query.whereClause, /ORDER BY candidate\.hall_queued_at ASC NULLS LAST/)
|
||||||
|
assert.match(query.whereClause, /LIMIT \$4/)
|
||||||
|
const candidateSql = query.whereClause.slice(query.whereClause.indexOf('candidate.status'))
|
||||||
|
assert.doesNotMatch(candidateSql, /candidate\.category_id|candidate.*测试商品/)
|
||||||
|
assert.deepEqual(query.params, [['open'], '%测试商品%', 3, 50, '2026-08-21T00:00:00.000Z'])
|
||||||
|
})
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ export type WorkOrderRow = {
|
|||||||
sharing_total_quantity: number
|
sharing_total_quantity: number
|
||||||
sharing_unit_reward: number
|
sharing_unit_reward: number
|
||||||
published_at: string | null
|
published_at: string | null
|
||||||
|
hall_queued_at: string | null
|
||||||
pinned_at: string | null
|
pinned_at: string | null
|
||||||
assigned_at: string | null
|
assigned_at: string | null
|
||||||
submitted_at: string | null
|
submitted_at: string | null
|
||||||
@@ -345,6 +346,8 @@ export type ListInput = {
|
|||||||
workerLevelIds?: number[]
|
workerLevelIds?: number[]
|
||||||
categoryId?: number
|
categoryId?: number
|
||||||
workerSharingId?: number
|
workerSharingId?: number
|
||||||
|
/** 大厅候选集合的总展示上限;筛选与分页在候选集合内执行。 */
|
||||||
|
hallCandidateLimit?: number
|
||||||
visibleAfterIso?: string
|
visibleAfterIso?: string
|
||||||
/** 抢单大厅隐藏已被拼完的工单。 */
|
/** 抢单大厅隐藏已被拼完的工单。 */
|
||||||
excludeFilledSharing?: boolean
|
excludeFilledSharing?: boolean
|
||||||
|
|||||||
@@ -271,6 +271,8 @@ export async function joinWorkOrderShare(input: {
|
|||||||
/** 立即抢单时在事务锁内承接全部剩余份数。 */
|
/** 立即抢单时在事务锁内承接全部剩余份数。 */
|
||||||
takeRemaining?: boolean
|
takeRemaining?: boolean
|
||||||
maxActiveOrders: number
|
maxActiveOrders: number
|
||||||
|
hallCandidateLimit?: number
|
||||||
|
visibleAfterIso?: string
|
||||||
now: string
|
now: string
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
share: WorkOrderShareRow | null
|
share: WorkOrderShareRow | null
|
||||||
@@ -299,6 +301,16 @@ export async function joinWorkOrderShare(input: {
|
|||||||
if (!workOrder || workOrder.status !== 'open') {
|
if (!workOrder || workOrder.status !== 'open') {
|
||||||
return { share: null, failureReason: 'work_order_not_open' }
|
return { share: null, failureReason: 'work_order_not_open' }
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
input.hallCandidateLimit &&
|
||||||
|
!(await isWorkOrderWithinHallCapacityWithClient(client, {
|
||||||
|
workOrderId: input.workOrderId,
|
||||||
|
hallCandidateLimit: input.hallCandidateLimit,
|
||||||
|
visibleAfterIso: input.visibleAfterIso,
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
return { share: null, failureReason: 'work_order_not_open' }
|
||||||
|
}
|
||||||
if (workOrder.sharing_enabled !== true) {
|
if (workOrder.sharing_enabled !== true) {
|
||||||
return { share: null, failureReason: 'sharing_disabled' }
|
return { share: null, failureReason: 'sharing_disabled' }
|
||||||
}
|
}
|
||||||
@@ -1291,6 +1303,7 @@ export async function listWorkOrders({
|
|||||||
workerLevelIds = [],
|
workerLevelIds = [],
|
||||||
categoryId = 0,
|
categoryId = 0,
|
||||||
workerSharingId = 0,
|
workerSharingId = 0,
|
||||||
|
hallCandidateLimit = 0,
|
||||||
visibleAfterIso = '',
|
visibleAfterIso = '',
|
||||||
excludeFilledSharing = false,
|
excludeFilledSharing = false,
|
||||||
vipEvidencePending = false,
|
vipEvidencePending = false,
|
||||||
@@ -1307,6 +1320,7 @@ export async function listWorkOrders({
|
|||||||
workerLevelIds,
|
workerLevelIds,
|
||||||
categoryId,
|
categoryId,
|
||||||
workerSharingId,
|
workerSharingId,
|
||||||
|
hallCandidateLimit,
|
||||||
visibleAfterIso,
|
visibleAfterIso,
|
||||||
excludeFilledSharing,
|
excludeFilledSharing,
|
||||||
vipEvidencePending,
|
vipEvidencePending,
|
||||||
@@ -1520,6 +1534,7 @@ export async function updateWorkOrder(
|
|||||||
| 'sharing_total_quantity'
|
| 'sharing_total_quantity'
|
||||||
| 'sharing_unit_reward'
|
| 'sharing_unit_reward'
|
||||||
| 'published_at'
|
| 'published_at'
|
||||||
|
| 'hall_queued_at'
|
||||||
| 'pinned_at'
|
| 'pinned_at'
|
||||||
| 'assigned_at'
|
| 'assigned_at'
|
||||||
| 'submitted_at'
|
| 'submitted_at'
|
||||||
@@ -1546,12 +1561,13 @@ export async function updateWorkOrder(
|
|||||||
sharing_total_quantity = $9,
|
sharing_total_quantity = $9,
|
||||||
sharing_unit_reward = $10,
|
sharing_unit_reward = $10,
|
||||||
published_at = $11,
|
published_at = $11,
|
||||||
pinned_at = $12,
|
hall_queued_at = $12,
|
||||||
assigned_at = $13,
|
pinned_at = $13,
|
||||||
submitted_at = $14,
|
assigned_at = $14,
|
||||||
accepted_at = $15,
|
submitted_at = $15,
|
||||||
updated_at = $16
|
accepted_at = $16,
|
||||||
WHERE id = $17
|
updated_at = $17
|
||||||
|
WHERE id = $18
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
next.status,
|
next.status,
|
||||||
@@ -1565,6 +1581,7 @@ export async function updateWorkOrder(
|
|||||||
toPositiveInteger(next.sharing_total_quantity, 1),
|
toPositiveInteger(next.sharing_total_quantity, 1),
|
||||||
toPositiveInteger(next.sharing_unit_reward, 0),
|
toPositiveInteger(next.sharing_unit_reward, 0),
|
||||||
next.published_at || null,
|
next.published_at || null,
|
||||||
|
next.hall_queued_at || null,
|
||||||
next.pinned_at || null,
|
next.pinned_at || null,
|
||||||
next.assigned_at || null,
|
next.assigned_at || null,
|
||||||
next.submitted_at || null,
|
next.submitted_at || null,
|
||||||
@@ -1642,6 +1659,8 @@ export async function grabWorkOrder(input: {
|
|||||||
workerId: number
|
workerId: number
|
||||||
depositAmount: number
|
depositAmount: number
|
||||||
maxActiveOrders: number
|
maxActiveOrders: number
|
||||||
|
hallCandidateLimit?: number
|
||||||
|
visibleAfterIso?: string
|
||||||
deadlineAt: string | null
|
deadlineAt: string | null
|
||||||
now: string
|
now: string
|
||||||
}): Promise<GrabWorkOrderResult> {
|
}): Promise<GrabWorkOrderResult> {
|
||||||
@@ -1664,6 +1683,20 @@ export async function grabWorkOrder(input: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
input.hallCandidateLimit &&
|
||||||
|
!(await isWorkOrderWithinHallCapacityWithClient(client, {
|
||||||
|
workOrderId: input.workOrderId,
|
||||||
|
hallCandidateLimit: input.hallCandidateLimit,
|
||||||
|
visibleAfterIso: input.visibleAfterIso,
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
order: null,
|
||||||
|
failureReason: 'work_order_not_open',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const result = await client.query<{ id: number }>(
|
const result = await client.query<{ id: number }>(
|
||||||
`
|
`
|
||||||
UPDATE work_orders
|
UPDATE work_orders
|
||||||
@@ -1887,6 +1920,7 @@ export async function cancelWorkerWorkOrder(input: {
|
|||||||
assigned_worker_id = NULL,
|
assigned_worker_id = NULL,
|
||||||
assigned_at = NULL,
|
assigned_at = NULL,
|
||||||
deadline_at = NULL,
|
deadline_at = NULL,
|
||||||
|
hall_queued_at = $1,
|
||||||
updated_at = $1
|
updated_at = $1
|
||||||
WHERE id = $2
|
WHERE id = $2
|
||||||
`,
|
`,
|
||||||
@@ -2370,7 +2404,8 @@ export async function reviewWorkerCancelRequest(input: {
|
|||||||
)
|
)
|
||||||
await client.query(
|
await client.query(
|
||||||
`UPDATE work_orders
|
`UPDATE work_orders
|
||||||
SET status = 'open', submitted_at = NULL, published_at = COALESCE(published_at, $1), updated_at = $1
|
SET status = 'open', submitted_at = NULL, published_at = COALESCE(published_at, $1),
|
||||||
|
hall_queued_at = COALESCE(hall_queued_at, $1), updated_at = $1
|
||||||
WHERE id = $2`,
|
WHERE id = $2`,
|
||||||
[input.now, workOrder.id],
|
[input.now, workOrder.id],
|
||||||
)
|
)
|
||||||
@@ -2401,7 +2436,7 @@ export async function reviewWorkerCancelRequest(input: {
|
|||||||
await client.query(
|
await client.query(
|
||||||
`UPDATE work_orders
|
`UPDATE work_orders
|
||||||
SET status = 'open', assigned_worker_id = NULL, assigned_at = NULL, deadline_at = NULL,
|
SET status = 'open', assigned_worker_id = NULL, assigned_at = NULL, deadline_at = NULL,
|
||||||
published_at = $1, updated_at = $1
|
published_at = $1, hall_queued_at = $1, updated_at = $1
|
||||||
WHERE id = $2`,
|
WHERE id = $2`,
|
||||||
[input.now, workOrder.id],
|
[input.now, workOrder.id],
|
||||||
)
|
)
|
||||||
@@ -2608,6 +2643,7 @@ export async function returnAssignedWorkOrderToHall(input: {
|
|||||||
assigned_at = NULL,
|
assigned_at = NULL,
|
||||||
deadline_at = NULL,
|
deadline_at = NULL,
|
||||||
published_at = $1,
|
published_at = $1,
|
||||||
|
hall_queued_at = $1,
|
||||||
updated_at = $1
|
updated_at = $1
|
||||||
WHERE id = $2
|
WHERE id = $2
|
||||||
`,
|
`,
|
||||||
@@ -3713,6 +3749,7 @@ export async function settleOverdueWorkOrder(input: {
|
|||||||
submitted_at = CASE WHEN $1 = 'open' THEN NULL ELSE submitted_at END,
|
submitted_at = CASE WHEN $1 = 'open' THEN NULL ELSE submitted_at END,
|
||||||
problem_note = CASE WHEN $1 = 'open' THEN '' ELSE problem_note END,
|
problem_note = CASE WHEN $1 = 'open' THEN '' ELSE problem_note END,
|
||||||
published_at = CASE WHEN $1 = 'open' THEN $2 ELSE published_at END,
|
published_at = CASE WHEN $1 = 'open' THEN $2 ELSE published_at END,
|
||||||
|
hall_queued_at = CASE WHEN $1 = 'open' THEN $2 ELSE hall_queued_at END,
|
||||||
updated_at = $2
|
updated_at = $2
|
||||||
WHERE id = $3
|
WHERE id = $3
|
||||||
`,
|
`,
|
||||||
@@ -4379,6 +4416,7 @@ export function buildWorkOrderWhere({
|
|||||||
workerLevelIds = [],
|
workerLevelIds = [],
|
||||||
categoryId = 0,
|
categoryId = 0,
|
||||||
workerSharingId = 0,
|
workerSharingId = 0,
|
||||||
|
hallCandidateLimit = 0,
|
||||||
visibleAfterIso = '',
|
visibleAfterIso = '',
|
||||||
excludeFilledSharing = false,
|
excludeFilledSharing = false,
|
||||||
vipEvidencePending = false,
|
vipEvidencePending = false,
|
||||||
@@ -4531,9 +4569,13 @@ export function buildWorkOrderWhere({
|
|||||||
params.push(categoryId)
|
params.push(categoryId)
|
||||||
filters.push(`wo.category_id = $${params.length}`)
|
filters.push(`wo.category_id = $${params.length}`)
|
||||||
}
|
}
|
||||||
|
if (hallCandidateLimit > 0) {
|
||||||
|
params.push(hallCandidateLimit)
|
||||||
|
filters.push(`wo.id IN (${buildHallCandidateIdsSql(`$${params.length}`)})`)
|
||||||
|
}
|
||||||
if (visibleAfterIso) {
|
if (visibleAfterIso) {
|
||||||
params.push(visibleAfterIso)
|
params.push(visibleAfterIso)
|
||||||
filters.push(`(wo.published_at IS NULL OR wo.published_at <= $${params.length})`)
|
filters.push(`(wo.hall_queued_at IS NULL OR wo.hall_queued_at <= $${params.length})`)
|
||||||
}
|
}
|
||||||
if (excludeFilledSharing) {
|
if (excludeFilledSharing) {
|
||||||
filters.push(`(
|
filters.push(`(
|
||||||
@@ -4548,6 +4590,52 @@ export function buildWorkOrderWhere({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 大厅容量先选出全局队首,分类、关键词和 VIP 延迟只能在该集合内继续筛选。 */
|
||||||
|
function buildHallCandidateIdsSql(limitPlaceholder: string) {
|
||||||
|
return `
|
||||||
|
SELECT candidate.id
|
||||||
|
FROM work_orders candidate
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
work_order_id,
|
||||||
|
SUM(quantity) FILTER (WHERE status != 'cancelled') AS active_share_quantity
|
||||||
|
FROM work_order_shares
|
||||||
|
GROUP BY work_order_id
|
||||||
|
) 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
|
||||||
|
)
|
||||||
|
ORDER BY candidate.hall_queued_at ASC NULLS LAST, candidate.id ASC
|
||||||
|
LIMIT ${limitPlaceholder}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 抢单操作在事务中复核容量与当前打手的可见延迟,避免通过订单 ID 绕过大厅队列。 */
|
||||||
|
async function isWorkOrderWithinHallCapacityWithClient(
|
||||||
|
client: PoolClient,
|
||||||
|
input: { workOrderId: number; hallCandidateLimit: number; visibleAfterIso?: string | undefined },
|
||||||
|
) {
|
||||||
|
const result = await client.query<{ visible: boolean }>(
|
||||||
|
`
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM work_orders wo
|
||||||
|
WHERE wo.id = $1
|
||||||
|
AND wo.id IN (${buildHallCandidateIdsSql('$2')})
|
||||||
|
AND ($3::timestamptz IS NULL OR wo.hall_queued_at IS NULL OR wo.hall_queued_at <= $3)
|
||||||
|
) AS visible
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
input.workOrderId,
|
||||||
|
input.hallCandidateLimit,
|
||||||
|
input.visibleAfterIso ? input.visibleAfterIso : null,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return result.rows[0]?.visible === true
|
||||||
|
}
|
||||||
|
|
||||||
function buildWorkProductRuleWhere({
|
function buildWorkProductRuleWhere({
|
||||||
enabled = null,
|
enabled = null,
|
||||||
keyword = '',
|
keyword = '',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
deleteAdminWorkOrder,
|
deleteAdminWorkOrder,
|
||||||
deleteAdminWorkerLevel,
|
deleteAdminWorkerLevel,
|
||||||
getAdminWorkerFinanceConfig,
|
getAdminWorkerFinanceConfig,
|
||||||
|
getAdminWorkerHallConfig,
|
||||||
getAdminWorkerProductMatchConfig,
|
getAdminWorkerProductMatchConfig,
|
||||||
getAdminWorkerAnnouncementConfig,
|
getAdminWorkerAnnouncementConfig,
|
||||||
getAdminWorkerPlatformNotificationConfig,
|
getAdminWorkerPlatformNotificationConfig,
|
||||||
@@ -46,6 +47,7 @@ import {
|
|||||||
reviewAdminWorkerUser,
|
reviewAdminWorkerUser,
|
||||||
resetAdminWorkerPassword,
|
resetAdminWorkerPassword,
|
||||||
saveAdminWorkerFinanceConfig,
|
saveAdminWorkerFinanceConfig,
|
||||||
|
saveAdminWorkerHallConfig,
|
||||||
saveAdminWorkerProductMatchConfig,
|
saveAdminWorkerProductMatchConfig,
|
||||||
saveAdminWorkerAnnouncementConfig,
|
saveAdminWorkerAnnouncementConfig,
|
||||||
saveAdminWorkerPlatformNotificationConfig,
|
saveAdminWorkerPlatformNotificationConfig,
|
||||||
@@ -285,6 +287,32 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/worker-platform/hall-config',
|
||||||
|
requireAdminRoles(['admin', 'operator']),
|
||||||
|
createJsonHandler(() => getAdminWorkerHallConfig(), {
|
||||||
|
successMessage: 'ok',
|
||||||
|
errorMessage: '读取大厅配置失败',
|
||||||
|
scope: '[admin/worker-platform/hall-config]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/worker-platform/hall-config',
|
||||||
|
requireAdminRoles(['admin', 'operator']),
|
||||||
|
createJsonHandler((req) => saveAdminWorkerHallConfig(req.body || {}), {
|
||||||
|
successMessage: '大厅配置已保存',
|
||||||
|
errorMessage: '保存大厅配置失败',
|
||||||
|
scope: '[admin/worker-platform/hall-config]',
|
||||||
|
audit: (_req, data) => ({
|
||||||
|
action: 'worker_hall_config_saved',
|
||||||
|
targetType: 'worker_hall_config',
|
||||||
|
targetId: 'worker_hall',
|
||||||
|
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/worker-platform/announcement',
|
'/worker-platform/announcement',
|
||||||
requireAdminRoles(['admin']),
|
requireAdminRoles(['admin']),
|
||||||
@@ -800,7 +828,7 @@ router.post(
|
|||||||
(req) =>
|
(req) =>
|
||||||
publishAdminWorkOrder(String(req.params.workOrderId || ''), req.adminSession?.username || ''),
|
publishAdminWorkOrder(String(req.params.workOrderId || ''), req.adminSession?.username || ''),
|
||||||
{
|
{
|
||||||
successMessage: '订单已发布到抢单大厅',
|
successMessage: '订单已进入大厅队列',
|
||||||
errorMessage: '发布接单订单失败',
|
errorMessage: '发布接单订单失败',
|
||||||
scope: '[admin/worker-platform/orders/:workOrderId/publish]',
|
scope: '[admin/worker-platform/orders/:workOrderId/publish]',
|
||||||
audit: (req, data) => ({
|
audit: (req, data) => ({
|
||||||
|
|||||||
@@ -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: {
|
export function publishWorkerFinanceRealtimeChange(input: {
|
||||||
requestId: number
|
requestId: number
|
||||||
@@ -282,17 +292,13 @@ async function publishWorkOrderSnapshots(input: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (input.hallChanged) {
|
if (input.hallChanged) {
|
||||||
const sharingFilled = isWorkOrderSharingFilled(
|
|
||||||
workOrder.sharing_enabled === true,
|
|
||||||
Number(workOrder.sharing_total_quantity || 1),
|
|
||||||
joinedQuantity,
|
|
||||||
)
|
|
||||||
publishRealtimeEvent({
|
publishRealtimeEvent({
|
||||||
type: 'work_order.changed',
|
type: 'work_order.changed',
|
||||||
entityId: workOrderId,
|
entityId: workOrderId,
|
||||||
scopes: ['worker_hall'],
|
scopes: ['worker_hall'],
|
||||||
operation: workOrder.status === 'open' && !sharingFilled ? 'upsert' : 'remove',
|
// 队首补位可能影响多张订单,客户端需要重新读取大厅快照。
|
||||||
data: mapWorkerHallWorkOrder(workOrder, shares),
|
operation: 'refresh',
|
||||||
|
data: { workOrderId },
|
||||||
broadcastWorkers: true,
|
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: {
|
async function publishWorkerFinanceSnapshots(input: {
|
||||||
requestId: number
|
requestId: number
|
||||||
workerId: number
|
workerId: number
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ import {
|
|||||||
getWorkerAnnouncementConfig,
|
getWorkerAnnouncementConfig,
|
||||||
saveWorkerAnnouncementConfig,
|
saveWorkerAnnouncementConfig,
|
||||||
} from './worker-announcement-config-service.js'
|
} 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 { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
|
||||||
import {
|
import {
|
||||||
createWorkOrderMaterialAdminNotification,
|
createWorkOrderMaterialAdminNotification,
|
||||||
@@ -98,6 +99,7 @@ import {
|
|||||||
} from '../admin/admin-notification-service.js'
|
} from '../admin/admin-notification-service.js'
|
||||||
import {
|
import {
|
||||||
publishWorkerFinanceRealtimeChange,
|
publishWorkerFinanceRealtimeChange,
|
||||||
|
publishWorkerHallRealtimeRefresh,
|
||||||
publishWorkerWalletRealtimeChange,
|
publishWorkerWalletRealtimeChange,
|
||||||
publishWorkOrderRealtimeChange,
|
publishWorkOrderRealtimeChange,
|
||||||
} from '../realtime/realtime-event-service.js'
|
} from '../realtime/realtime-event-service.js'
|
||||||
@@ -2235,6 +2237,7 @@ export async function publishAdminWorkOrder(workOrderId: number | string, actorN
|
|||||||
const updated = await updateWorkOrder(workOrder.id, {
|
const updated = await updateWorkOrder(workOrder.id, {
|
||||||
status: WORK_ORDER_STATUS.OPEN,
|
status: WORK_ORDER_STATUS.OPEN,
|
||||||
published_at: now,
|
published_at: now,
|
||||||
|
hall_queued_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
})
|
})
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
@@ -2260,7 +2263,20 @@ export async function publishAdminWorkOrder(workOrderId: number | string, actorN
|
|||||||
now,
|
now,
|
||||||
})
|
})
|
||||||
publishWorkOrderRealtimeChange({ workOrderId: Number(workOrder.id), hallChanged: true })
|
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 = '') {
|
export async function unpublishAdminWorkOrder(workOrderId: number | string, actorName = '') {
|
||||||
@@ -2698,6 +2714,7 @@ async function resolveWorkOrderRealtimeWorkerIds(workOrder: WorkOrderRow) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getAdminWorkerPlatformSummary() {
|
export async function getAdminWorkerPlatformSummary() {
|
||||||
|
const hallConfig = getWorkerHallConfig()
|
||||||
const [pendingWorkers, pendingMaterial, openOrders, inProgressOrders] = await Promise.all([
|
const [pendingWorkers, pendingMaterial, openOrders, inProgressOrders] = await Promise.all([
|
||||||
listWorkerUsers({ page: 1, pageSize: 1, status: 'pending_review' }),
|
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.PENDING_MATERIAL] }),
|
||||||
@@ -2717,11 +2734,35 @@ export async function getAdminWorkerPlatformSummary() {
|
|||||||
return {
|
return {
|
||||||
pendingWorkerCount: pendingWorkers.total,
|
pendingWorkerCount: pendingWorkers.total,
|
||||||
pendingMaterialCount: pendingMaterial.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,
|
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(
|
export async function syncWorkerOrdersForSourceOrder(
|
||||||
order: OrderRow,
|
order: OrderRow,
|
||||||
orderItems: OrderItemRow[],
|
orderItems: OrderItemRow[],
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ export * from './worker-service.js'
|
|||||||
export * from './admin-service.js'
|
export * from './admin-service.js'
|
||||||
export * from './worker-product-match-config-service.js'
|
export * from './worker-product-match-config-service.js'
|
||||||
export * from './worker-announcement-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_total_quantity: 1,
|
||||||
sharing_unit_reward: 0,
|
sharing_unit_reward: 0,
|
||||||
published_at: null,
|
published_at: null,
|
||||||
|
hall_queued_at: null,
|
||||||
pinned_at: null,
|
pinned_at: null,
|
||||||
assigned_at: null,
|
assigned_at: null,
|
||||||
submitted_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: '{}',
|
acceptance_json: '{}',
|
||||||
problem_note: '',
|
problem_note: '',
|
||||||
published_at: null,
|
published_at: null,
|
||||||
|
hall_queued_at: null,
|
||||||
assigned_at: null,
|
assigned_at: null,
|
||||||
submitted_at: null,
|
submitted_at: null,
|
||||||
accepted_at: null,
|
accepted_at: null,
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-
|
|||||||
import { getSmsProvider } from '../sms/index.js'
|
import { getSmsProvider } from '../sms/index.js'
|
||||||
import { getWorkerFinanceConfig } from './worker-finance-config-service.js'
|
import { getWorkerFinanceConfig } from './worker-finance-config-service.js'
|
||||||
import { getWorkerAnnouncementConfig } from './worker-announcement-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 { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
|
||||||
import {
|
import {
|
||||||
createWorkerAcceptanceAdminNotification,
|
createWorkerAcceptanceAdminNotification,
|
||||||
@@ -1470,12 +1471,14 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work
|
|||||||
const visibleDelaySeconds = Number(permissions.visibleDelaySeconds || 0)
|
const visibleDelaySeconds = Number(permissions.visibleDelaySeconds || 0)
|
||||||
const visibleAfterIso =
|
const visibleAfterIso =
|
||||||
visibleDelaySeconds > 0 ? new Date(Date.now() - visibleDelaySeconds * 1000).toISOString() : ''
|
visibleDelaySeconds > 0 ? new Date(Date.now() - visibleDelaySeconds * 1000).toISOString() : ''
|
||||||
|
const hallConfig = getWorkerHallConfig()
|
||||||
const { items, total } = await listWorkOrders({
|
const { items, total } = await listWorkOrders({
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
statuses: [WORK_ORDER_STATUS.OPEN],
|
statuses: [WORK_ORDER_STATUS.OPEN],
|
||||||
keyword: String(query.keyword || '').trim(),
|
keyword: String(query.keyword || '').trim(),
|
||||||
categoryId: categoryId || 0,
|
categoryId: categoryId || 0,
|
||||||
|
hallCandidateLimit: hallConfig.maxVisibleOrders,
|
||||||
visibleAfterIso,
|
visibleAfterIso,
|
||||||
excludeFilledSharing: true,
|
excludeFilledSharing: true,
|
||||||
pinnedFirst: true,
|
pinnedFirst: true,
|
||||||
@@ -1555,6 +1558,11 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session:
|
|||||||
requireActiveWorkerSession(session)
|
requireActiveWorkerSession(session)
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
const permissions = resolveWorkerPermissions(worker)
|
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)
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
if (workOrder.status !== WORK_ORDER_STATUS.OPEN || workOrder.assigned_worker_id) {
|
if (workOrder.status !== WORK_ORDER_STATUS.OPEN || workOrder.assigned_worker_id) {
|
||||||
throw createHttpError('该订单已不可抢', {
|
throw createHttpError('该订单已不可抢', {
|
||||||
@@ -1570,7 +1578,9 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session:
|
|||||||
quantity: 0,
|
quantity: 0,
|
||||||
takeRemaining: true,
|
takeRemaining: true,
|
||||||
maxActiveOrders: permissions.maxActiveOrders,
|
maxActiveOrders: permissions.maxActiveOrders,
|
||||||
now: nowIso(),
|
hallCandidateLimit: hallConfig.maxVisibleOrders,
|
||||||
|
visibleAfterIso,
|
||||||
|
now,
|
||||||
})
|
})
|
||||||
if (!result.share) {
|
if (!result.share) {
|
||||||
throw resolveWorkOrderShareJoinError(result.failureReason)
|
throw resolveWorkOrderShareJoinError(result.failureReason)
|
||||||
@@ -1604,8 +1614,10 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session:
|
|||||||
workerId: Number(worker.id),
|
workerId: Number(worker.id),
|
||||||
depositAmount: freezeAmount,
|
depositAmount: freezeAmount,
|
||||||
maxActiveOrders: permissions.maxActiveOrders,
|
maxActiveOrders: permissions.maxActiveOrders,
|
||||||
deadlineAt: resolveWorkOrderDeadlineAt(workOrder, nowIso()),
|
hallCandidateLimit: hallConfig.maxVisibleOrders,
|
||||||
now: nowIso(),
|
visibleAfterIso,
|
||||||
|
deadlineAt: resolveWorkOrderDeadlineAt(workOrder, now),
|
||||||
|
now,
|
||||||
})
|
})
|
||||||
if (!grabbed.order) {
|
if (!grabbed.order) {
|
||||||
throwGrabWorkOrderFailure(grabbed.failureReason)
|
throwGrabWorkOrderFailure(grabbed.failureReason)
|
||||||
@@ -2036,6 +2048,10 @@ export async function joinWorkerSharingOrder(
|
|||||||
requireActiveWorkerSession(session)
|
requireActiveWorkerSession(session)
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
const permissions = resolveWorkerPermissions(worker)
|
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 quantity = normalizePositiveInteger(payload.quantity, 1)
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
const result = await joinWorkOrderShare({
|
const result = await joinWorkOrderShare({
|
||||||
@@ -2043,6 +2059,8 @@ export async function joinWorkerSharingOrder(
|
|||||||
workerId: Number(worker.id),
|
workerId: Number(worker.id),
|
||||||
quantity,
|
quantity,
|
||||||
maxActiveOrders: permissions.maxActiveOrders,
|
maxActiveOrders: permissions.maxActiveOrders,
|
||||||
|
hallCandidateLimit: hallConfig.maxVisibleOrders,
|
||||||
|
visibleAfterIso,
|
||||||
now,
|
now,
|
||||||
})
|
})
|
||||||
if (!result.share) {
|
if (!result.share) {
|
||||||
|
|||||||
@@ -224,6 +224,10 @@ function patchWorkerHallCaches(
|
|||||||
queryClient: ReturnType<typeof useQueryClient>,
|
queryClient: ReturnType<typeof useQueryClient>,
|
||||||
event: RealtimeEvent,
|
event: RealtimeEvent,
|
||||||
) {
|
) {
|
||||||
|
if (event.operation === 'refresh') {
|
||||||
|
void queryClient.resetQueries({ queryKey: ['worker-hall-orders'] })
|
||||||
|
return
|
||||||
|
}
|
||||||
const workerSnapshot = applyWorkerDepositRule(queryClient, event.data)
|
const workerSnapshot = applyWorkerDepositRule(queryClient, event.data)
|
||||||
const item = workerSnapshot as {
|
const item = workerSnapshot as {
|
||||||
status?: unknown
|
status?: unknown
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import AnnouncementPanel from './panels/AnnouncementPanel'
|
|||||||
import CancelRequestsPanel from './panels/CancelRequestsPanel'
|
import CancelRequestsPanel from './panels/CancelRequestsPanel'
|
||||||
import FinancePanel from './panels/FinancePanel'
|
import FinancePanel from './panels/FinancePanel'
|
||||||
import FeedbacksPanel from './panels/FeedbacksPanel'
|
import FeedbacksPanel from './panels/FeedbacksPanel'
|
||||||
|
import HallConfigPanel from './panels/HallConfigPanel'
|
||||||
import LevelsPanel from './panels/LevelsPanel'
|
import LevelsPanel from './panels/LevelsPanel'
|
||||||
import NotificationsPanel from './panels/NotificationsPanel'
|
import NotificationsPanel from './panels/NotificationsPanel'
|
||||||
import ProductRulesPanel from './panels/ProductRulesPanel'
|
import ProductRulesPanel from './panels/ProductRulesPanel'
|
||||||
@@ -66,6 +67,7 @@ export default function AdminWorkerPlatformPage() {
|
|||||||
{ key: 'workers', label: '打手', children: <WorkersPanel /> },
|
{ key: 'workers', label: '打手', children: <WorkersPanel /> },
|
||||||
{ key: 'finance', label: '资金', children: <FinancePanel /> },
|
{ key: 'finance', label: '资金', children: <FinancePanel /> },
|
||||||
{ key: 'levels', label: '等级权限', children: <LevelsPanel /> },
|
{ key: 'levels', label: '等级权限', children: <LevelsPanel /> },
|
||||||
|
{ key: 'hall', label: '大厅配置', children: <HallConfigPanel /> },
|
||||||
...(isAdmin
|
...(isAdmin
|
||||||
? [{ key: 'notifications', label: '通知配置', children: <NotificationsPanel /> }]
|
? [{ key: 'notifications', label: '通知配置', children: <NotificationsPanel /> }]
|
||||||
: []),
|
: []),
|
||||||
@@ -90,6 +92,7 @@ function loadActiveTabPreference(): string {
|
|||||||
saved === 'workers' ||
|
saved === 'workers' ||
|
||||||
saved === 'finance' ||
|
saved === 'finance' ||
|
||||||
saved === 'levels' ||
|
saved === 'levels' ||
|
||||||
|
saved === 'hall' ||
|
||||||
saved === 'notifications'
|
saved === 'notifications'
|
||||||
? saved
|
? saved
|
||||||
: 'orders'
|
: 'orders'
|
||||||
@@ -111,6 +114,7 @@ function isWorkerPlatformTab(tab: string | null): tab is string {
|
|||||||
'workers',
|
'workers',
|
||||||
'finance',
|
'finance',
|
||||||
'levels',
|
'levels',
|
||||||
|
'hall',
|
||||||
'notifications',
|
'notifications',
|
||||||
].includes(String(tab || ''))
|
].includes(String(tab || ''))
|
||||||
}
|
}
|
||||||
@@ -125,7 +129,11 @@ function SummaryCards() {
|
|||||||
const items = [
|
const items = [
|
||||||
{ label: '待处理打手', value: summary?.pendingWorkerCount || 0 },
|
{ label: '待处理打手', value: summary?.pendingWorkerCount || 0 },
|
||||||
{ label: '待完善订单', value: summary?.pendingMaterialCount || 0 },
|
{ label: '待完善订单', value: summary?.pendingMaterialCount || 0 },
|
||||||
{ label: '大厅订单', value: summary?.openOrderCount || 0 },
|
{
|
||||||
|
label: '大厅展示',
|
||||||
|
value: `${summary?.openOrderCount || 0}/${summary?.hallMaxVisibleOrders || 0}`,
|
||||||
|
},
|
||||||
|
{ label: '等待入厅', value: summary?.queuedHallOrderCount || 0 },
|
||||||
{ label: '代练中', value: summary?.inProgressOrderCount || 0 },
|
{ label: '代练中', value: summary?.inProgressOrderCount || 0 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { App, Button, Card, Descriptions, Form, InputNumber, Space } from 'antd'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { fetchAdminWorkerHallConfig, saveAdminWorkerHallConfig } from '@/services/admin'
|
||||||
|
|
||||||
|
type HallConfigFormValues = {
|
||||||
|
maxVisibleOrders: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HallConfigPanel() {
|
||||||
|
const { message } = App.useApp()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [form] = Form.useForm<HallConfigFormValues>()
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const configQuery = useQuery({
|
||||||
|
queryKey: ['admin-worker-hall-config'],
|
||||||
|
queryFn: fetchAdminWorkerHallConfig,
|
||||||
|
})
|
||||||
|
const config = configQuery.data?.data
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (config) form.setFieldsValue({ maxVisibleOrders: config.maxVisibleOrders })
|
||||||
|
}, [config, form])
|
||||||
|
|
||||||
|
async function saveConfig(values: HallConfigFormValues) {
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
await saveAdminWorkerHallConfig({ maxVisibleOrders: Number(values.maxVisibleOrders) })
|
||||||
|
await Promise.all([
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['admin-worker-hall-config'] }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-summary'] }),
|
||||||
|
])
|
||||||
|
message.success('大厅配置已保存')
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '保存大厅配置失败')
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="大厅配置" bordered={false} loading={configQuery.isLoading}>
|
||||||
|
<Space direction="vertical" size="middle" style={{ width: '100%', maxWidth: 560 }}>
|
||||||
|
<Descriptions size="small" column={3}>
|
||||||
|
<Descriptions.Item label="展示中">{config?.visibleOrderCount || 0}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="等待入厅">{config?.queuedOrderCount || 0}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="当前上限">{config?.maxVisibleOrders || '-'}</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
<Form form={form} layout="inline" onFinish={saveConfig}>
|
||||||
|
<Form.Item
|
||||||
|
name="maxVisibleOrders"
|
||||||
|
label="大厅订单总数"
|
||||||
|
rules={[{ required: true, message: '请输入大厅订单总数' }]}
|
||||||
|
>
|
||||||
|
<InputNumber min={1} max={500} precision={0} style={{ width: 160 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" loading={saving}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -438,9 +438,11 @@ export default function WorkOrdersPanel() {
|
|||||||
return runAction(
|
return runAction(
|
||||||
() => publishAdminWorkOrder(row.workOrderId),
|
() => publishAdminWorkOrder(row.workOrderId),
|
||||||
(response) =>
|
(response) =>
|
||||||
response.data.voucherConsume.voucherCount > 0
|
response.data.hallVisible
|
||||||
? '电子凭证已自动核销,订单已发布到大厅'
|
? response.data.voucherConsume.voucherCount > 0
|
||||||
: '订单已发布到大厅',
|
? '电子凭证已自动核销,订单已展示在大厅'
|
||||||
|
: '订单已展示在大厅'
|
||||||
|
: '订单已进入大厅等待队列',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
KuaishouMatchSource,
|
KuaishouMatchSource,
|
||||||
WorkProductMatchLog,
|
WorkProductMatchLog,
|
||||||
WorkerFinanceConfig,
|
WorkerFinanceConfig,
|
||||||
|
WorkerHallConfig,
|
||||||
WorkerFinanceRequest,
|
WorkerFinanceRequest,
|
||||||
WorkerCancelRequest,
|
WorkerCancelRequest,
|
||||||
WorkerOrderFeedback,
|
WorkerOrderFeedback,
|
||||||
@@ -38,10 +39,20 @@ export function fetchAdminWorkerPlatformSummary() {
|
|||||||
pendingWorkerCount: number
|
pendingWorkerCount: number
|
||||||
pendingMaterialCount: number
|
pendingMaterialCount: number
|
||||||
openOrderCount: number
|
openOrderCount: number
|
||||||
|
queuedHallOrderCount: number
|
||||||
|
hallMaxVisibleOrders: number
|
||||||
inProgressOrderCount: number
|
inProgressOrderCount: number
|
||||||
}>('/api/v1/admin/worker-platform/summary')
|
}>('/api/v1/admin/worker-platform/summary')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchAdminWorkerHallConfig() {
|
||||||
|
return apiGet<WorkerHallConfig>('/api/v1/admin/worker-platform/hall-config')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveAdminWorkerHallConfig(payload: Pick<WorkerHallConfig, 'maxVisibleOrders'>) {
|
||||||
|
return apiPost<WorkerHallConfig>('/api/v1/admin/worker-platform/hall-config', payload)
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchAdminWorkerAnnouncement() {
|
export function fetchAdminWorkerAnnouncement() {
|
||||||
return apiGet<WorkerAnnouncementConfig>('/api/v1/admin/worker-platform/announcement')
|
return apiGet<WorkerAnnouncementConfig>('/api/v1/admin/worker-platform/announcement')
|
||||||
}
|
}
|
||||||
@@ -454,6 +465,7 @@ export function publishAdminWorkOrder(workOrderId: number) {
|
|||||||
return apiPost<{
|
return apiPost<{
|
||||||
order: WorkOrder
|
order: WorkOrder
|
||||||
voucherConsume: WorkOrderVoucherConsumeResult
|
voucherConsume: WorkOrderVoucherConsumeResult
|
||||||
|
hallVisible: boolean
|
||||||
}>(`/api/v1/admin/worker-platform/orders/${workOrderId}/publish`)
|
}>(`/api/v1/admin/worker-platform/orders/${workOrderId}/publish`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -199,6 +199,12 @@ export type WorkerAnnouncementConfig = {
|
|||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkerHallConfig = {
|
||||||
|
maxVisibleOrders: number
|
||||||
|
visibleOrderCount: number
|
||||||
|
queuedOrderCount: number
|
||||||
|
}
|
||||||
|
|
||||||
export type WorkerWithdrawalAccount = {
|
export type WorkerWithdrawalAccount = {
|
||||||
accountChannel: string
|
accountChannel: string
|
||||||
accountName: string
|
accountName: string
|
||||||
|
|||||||
Reference in New Issue
Block a user