diff --git a/apps/backend/src/services/worker-platform/index.ts b/apps/backend/src/services/worker-platform/index.ts index 33844664..be78311c 100644 --- a/apps/backend/src/services/worker-platform/index.ts +++ b/apps/backend/src/services/worker-platform/index.ts @@ -15,6 +15,7 @@ export * from './worker-order-realtime-service.js' export * from './worker-collect-service.js' export * from './worker-acceptance-service.js' export * from './worker-my-order-service.js' +export * from './worker-hall-action-service.js' export * from './worker-platform-defaults.js' export * from './work-order-timeout-policy.js' export * from './after-sales-service.js' diff --git a/apps/backend/src/services/worker-platform/worker-hall-action-service.ts b/apps/backend/src/services/worker-platform/worker-hall-action-service.ts new file mode 100644 index 00000000..3310e6c9 --- /dev/null +++ b/apps/backend/src/services/worker-platform/worker-hall-action-service.ts @@ -0,0 +1,188 @@ +import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js' +import { + countWorkerActiveOrders, + createWorkOrderEvent, + grabWorkOrder, + joinWorkOrderShare, + type WorkOrderRow, +} from '../../repositories/worker-platform/index.js' +import type { JsonObject } from '../../types/json.js' +import { createHttpError } from '../../utils/http.js' +import { nowIso } from '../../utils/time.js' +import { + mapWorkOrderForWorker, + mapWorkOrderShare, + normalizePositiveInteger, + resolveFreezeDepositAmount, + resolveWorkerPermissions, + throwGrabWorkOrderFailure, +} from './mappers.js' +import { getWorkerHallConfig } from './worker-hall-config-service.js' +import { publishWorkOrderParticipantChange } from './worker-order-realtime-service.js' +import { + getRequiredWorkOrder, + getRequiredWorker, + requireActiveWorkerSession, + type WorkerSession, +} from './worker-session-context-service.js' +import { + publishWorkerWalletRealtimeChange, + publishWorkOrderRealtimeChange, +} from '../realtime/realtime-event-service.js' + +export async function grabWorkerHallOrder(workOrderId: number | string, session: WorkerSession) { + 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('该订单已不可抢', { + statusCode: 409, + errorCode: 'work_order_not_open', + }) + } + + if (workOrder.sharing_enabled) { + const result = await joinWorkOrderShare({ + workOrderId: Number(workOrder.id), + workerId: Number(worker.id), + quantity: 0, + takeRemaining: true, + maxActiveOrders: permissions.maxActiveOrders, + hallCandidateLimit: hallConfig.maxVisibleOrders, + visibleAfterIso, + now, + }) + if (!result.share) { + throw resolveWorkOrderShareJoinError(result.failureReason) + } + await publishWorkOrderParticipantChange(Number(workOrder.id), { + workerIds: [Number(worker.id)], + hallChanged: true, + walletChangedWorkerIds: [Number(worker.id)], + }) + return { order: mapWorkOrderForWorker(workOrder, permissions, result.share) } + } + + const activeCount = await countWorkerActiveOrders(worker.id) + if (activeCount >= permissions.maxActiveOrders) { + throw createHttpError('已达到当前等级最大同时接单量', { + statusCode: 409, + errorCode: 'worker_active_order_limit', + }) + } + + const freezeAmount = resolveFreezeDepositAmount(workOrder, permissions) + if (Number(worker.available_amount || 0) < freezeAmount) { + throw createHttpError('余额不足,无法冻结所需押金', { + statusCode: 409, + errorCode: 'worker_deposit_insufficient', + }) + } + + const grabbed = await grabWorkOrder({ + workOrderId: Number(workOrder.id), + workerId: Number(worker.id), + depositAmount: freezeAmount, + maxActiveOrders: permissions.maxActiveOrders, + hallCandidateLimit: hallConfig.maxVisibleOrders, + visibleAfterIso, + deadlineAt: resolveWorkOrderDeadlineAt(workOrder, now), + now, + }) + if (!grabbed.order) { + throwGrabWorkOrderFailure(grabbed.failureReason) + } + + await createWorkOrderEvent({ + workOrderId: workOrder.id, + actorType: 'worker', + actorId: String(worker.id), + eventType: 'grabbed', + fromStatus: workOrder.status, + toStatus: grabbed.order.status, + payloadJson: JSON.stringify({ + workerId: worker.id, + workerName: worker.display_name || worker.username, + internal: worker.worker_type === 'internal', + }), + now: nowIso(), + }) + publishWorkOrderRealtimeChange({ + workOrderId: Number(workOrder.id), + workerIds: [Number(worker.id)], + hallChanged: true, + }) + publishWorkerWalletRealtimeChange(Number(worker.id)) + + return { order: mapWorkOrderForWorker(grabbed.order, permissions) } +} + +export async function joinWorkerSharingOrder( + workOrderId: number | string, + payload: JsonObject = {}, + session: WorkerSession, +) { + 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({ + workOrderId: Number(workOrderId), + workerId: Number(worker.id), + quantity, + maxActiveOrders: permissions.maxActiveOrders, + hallCandidateLimit: hallConfig.maxVisibleOrders, + visibleAfterIso, + now, + }) + if (!result.share) { + throw resolveWorkOrderShareJoinError(result.failureReason) + } + await publishWorkOrderParticipantChange(Number(workOrderId), { + workerIds: [Number(worker.id)], + hallChanged: true, + walletChangedWorkerIds: [Number(worker.id)], + }) + return { share: mapWorkOrderShare(result.share) } +} + +function resolveWorkOrderDeadlineAt(workOrder: WorkOrderRow, now: string): string | null { + const timeoutMinutes = Number(workOrder.timeout_minutes || 0) + if (timeoutMinutes <= 0) return null + return new Date(new Date(now).getTime() + timeoutMinutes * 60 * 1000).toISOString() +} + +function resolveWorkOrderShareJoinError( + failureReason: + | 'work_order_not_open' + | 'sharing_disabled' + | 'work_order_owner_conflict' + | 'sharing_quantity_full' + | 'worker_active_order_limit' + | 'worker_deposit_insufficient' + | null, +): Error { + const messages: Record = { + work_order_not_open: '该订单当前不可拼单', + sharing_disabled: '该订单未启用拼单', + work_order_owner_conflict: '你已整单接取该订单,无需拼单', + sharing_quantity_full: '拼单数量已满或超出剩余数量', + worker_active_order_limit: '已达到当前等级最大同时接单量', + worker_deposit_insufficient: '余额不足,无法冻结所需押金', + } + return createHttpError(messages[failureReason || ''] || '拼单失败', { + statusCode: 409, + errorCode: `work_order_sharing_${failureReason || 'failed'}`, + }) +} diff --git a/apps/backend/src/services/worker-platform/worker-service.ts b/apps/backend/src/services/worker-platform/worker-service.ts index b249400e..c9fcdb86 100644 --- a/apps/backend/src/services/worker-platform/worker-service.ts +++ b/apps/backend/src/services/worker-platform/worker-service.ts @@ -1,78 +1,3 @@ -import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js' -import { - countWorkerActiveOrders, - consumeWorkerSmsCode, - createWorkOrderEvent, - getLatestWorkerSmsCode, - recordWorkerSmsCodeFailure, - getWorkerUserById, - getWorkerUserByPhone, - getWorkerUserByUsername, - getWorkerUserByDisplayName, - getWorkOrderById, - grabWorkOrder, - createWorkerSessionRecord, - getWorkerSessionBySessionId, - listWorkerSessions, - revokeWorkerSession, - touchWorkerSession, - joinWorkOrderShare, - type WorkOrderRow, - type WorkerSessionRow, -} from '../../repositories/worker-platform/index.js' -import type { JsonObject } from '../../types/json.js' -import { createHttpError } from '../../utils/http.js' -import { nowIso } from '../../utils/time.js' -import { getWorkerHallConfig } from './worker-hall-config-service.js' -import { - getRequiredWorkOrder, - getRequiredWorker, - requireActiveWorkerSession, - type WorkerSession, -} from './worker-session-context-service.js' -import { - publishWorkerWalletRealtimeChange, - publishWorkOrderRealtimeChange, -} from '../realtime/realtime-event-service.js' - -import { - mapWorkOrderForWorker, - mapWorkOrderShare, - normalizePositiveInteger, - resolveFreezeDepositAmount, - resolveWorkerPermissions, - throwGrabWorkOrderFailure, -} from './mappers.js' -import { - assertWorkerLoginAllowed, - normalizeWorkerDeviceId, - normalizeWorkerDeviceName, - normalizeWorkerDeviceType, -} from './worker-auth-policy.js' -import { ensureWorkerPlatformDefaults } from './worker-platform-defaults.js' -import { - deleteWorkerSessionWithCredentials, - loginWorker, - loginWorkerBySmsCode, - manageWorkerSessions, -} from './worker-login-service.js' -import { registerWorker, resetWorkerPassword } from './worker-registration-service.js' -import { - sendWorkerLoginSmsCode, - sendWorkerPasswordResetSmsCode, - sendWorkerSmsCode, -} from './worker-sms-service.js' -import { changeWorkerPassword } from './worker-credentials-service.js' -import { - deleteWorkerSession, - getWorkerSessionSummary, - getWorkerSessions, - logoutWorkerSession, - verifyWorkerSessionToken, -} from './worker-session-auth-service.js' -import { normalizeWorkOrderTimeoutPolicy } from './work-order-timeout-policy.js' -import { publishWorkOrderParticipantChange } from './worker-order-realtime-service.js' - export { getRequiredWorkOrder, getRequiredWorker, @@ -124,104 +49,7 @@ export { maskWorkerLeaderboardDisplayName, } from './worker-hall-service.js' -export async function grabWorkerHallOrder(workOrderId: number | string, session: WorkerSession) { - 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('该订单已不可抢', { - statusCode: 409, - errorCode: 'work_order_not_open', - }) - } - - if (workOrder.sharing_enabled) { - const result = await joinWorkOrderShare({ - workOrderId: Number(workOrder.id), - workerId: Number(worker.id), - quantity: 0, - takeRemaining: true, - maxActiveOrders: permissions.maxActiveOrders, - hallCandidateLimit: hallConfig.maxVisibleOrders, - visibleAfterIso, - now, - }) - if (!result.share) { - throw resolveWorkOrderShareJoinError(result.failureReason) - } - await publishWorkOrderParticipantChange(Number(workOrder.id), { - workerIds: [Number(worker.id)], - hallChanged: true, - walletChangedWorkerIds: [Number(worker.id)], - }) - return { order: mapWorkOrderForWorker(workOrder, permissions, result.share) } - } - - const activeCount = await countWorkerActiveOrders(worker.id) - if (activeCount >= permissions.maxActiveOrders) { - throw createHttpError('已达到当前等级最大同时接单量', { - statusCode: 409, - errorCode: 'worker_active_order_limit', - }) - } - - const freezeAmount = resolveFreezeDepositAmount(workOrder, permissions) - if (Number(worker.available_amount || 0) < freezeAmount) { - throw createHttpError('余额不足,无法冻结所需押金', { - statusCode: 409, - errorCode: 'worker_deposit_insufficient', - }) - } - - const grabbed = await grabWorkOrder({ - workOrderId: Number(workOrder.id), - workerId: Number(worker.id), - depositAmount: freezeAmount, - maxActiveOrders: permissions.maxActiveOrders, - hallCandidateLimit: hallConfig.maxVisibleOrders, - visibleAfterIso, - deadlineAt: resolveWorkOrderDeadlineAt(workOrder, now), - now, - }) - if (!grabbed.order) { - throwGrabWorkOrderFailure(grabbed.failureReason) - } - - await createWorkOrderEvent({ - workOrderId: workOrder.id, - actorType: 'worker', - actorId: String(worker.id), - eventType: 'grabbed', - fromStatus: workOrder.status, - toStatus: grabbed.order.status, - payloadJson: JSON.stringify({ - workerId: worker.id, - workerName: worker.display_name || worker.username, - internal: worker.worker_type === 'internal', - }), - now: nowIso(), - }) - publishWorkOrderRealtimeChange({ - workOrderId: Number(workOrder.id), - workerIds: [Number(worker.id)], - hallChanged: true, - }) - publishWorkerWalletRealtimeChange(Number(worker.id)) - - return { order: mapWorkOrderForWorker(grabbed.order, permissions) } -} - -function resolveWorkOrderDeadlineAt(workOrder: WorkOrderRow, now: string): string | null { - const timeoutMinutes = Number(workOrder.timeout_minutes || 0) - if (timeoutMinutes <= 0) return null - return new Date(new Date(now).getTime() + timeoutMinutes * 60 * 1000).toISOString() -} +export { grabWorkerHallOrder } from './worker-hall-action-service.js' export { settleDueDepositUnfreezes } from './worker-profile-service.js' @@ -240,63 +68,7 @@ export { submitWorkerOrderFeedback, } from './worker-my-order-service.js' -export async function joinWorkerSharingOrder( - workOrderId: number | string, - payload: JsonObject = {}, - session: WorkerSession, -) { - 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({ - workOrderId: Number(workOrderId), - workerId: Number(worker.id), - quantity, - maxActiveOrders: permissions.maxActiveOrders, - hallCandidateLimit: hallConfig.maxVisibleOrders, - visibleAfterIso, - now, - }) - if (!result.share) { - throw resolveWorkOrderShareJoinError(result.failureReason) - } - await publishWorkOrderParticipantChange(Number(workOrderId), { - workerIds: [Number(worker.id)], - hallChanged: true, - walletChangedWorkerIds: [Number(worker.id)], - }) - return { share: mapWorkOrderShare(result.share) } -} - -function resolveWorkOrderShareJoinError( - failureReason: - | 'work_order_not_open' - | 'sharing_disabled' - | 'work_order_owner_conflict' - | 'sharing_quantity_full' - | 'worker_active_order_limit' - | 'worker_deposit_insufficient' - | null, -): Error { - const messages: Record = { - work_order_not_open: '该订单当前不可拼单', - sharing_disabled: '该订单未启用拼单', - work_order_owner_conflict: '你已整单接取该订单,无需拼单', - sharing_quantity_full: '拼单数量已满或超出剩余数量', - worker_active_order_limit: '已达到当前等级最大同时接单量', - worker_deposit_insufficient: '余额不足,无法冻结所需押金', - } - return createHttpError(messages[failureReason || ''] || '拼单失败', { - statusCode: 409, - errorCode: `work_order_sharing_${failureReason || 'failed'}`, - }) -} +export { joinWorkerSharingOrder } from './worker-hall-action-service.js' export { remindWorkerOrderAcceptance,