From 8388d6e875f48deb6e11fd9fc3a1720854130cd5 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 21 Aug 2026 16:48:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E6=89=93=E6=89=8B=E8=B5=84?= =?UTF-8?q?=E6=96=99=E8=B5=84=E9=87=91=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/deposit-unfreeze-job.ts | 2 +- .../src/services/worker-platform/index.ts | 1 + .../worker-platform/worker-profile-service.ts | 547 ++++++++++++++++++ .../worker-platform/worker-service.ts | 545 +---------------- 4 files changed, 560 insertions(+), 535 deletions(-) create mode 100644 apps/backend/src/services/worker-platform/worker-profile-service.ts diff --git a/apps/backend/src/services/scheduler/deposit-unfreeze-job.ts b/apps/backend/src/services/scheduler/deposit-unfreeze-job.ts index 717338d9..040a3aa8 100644 --- a/apps/backend/src/services/scheduler/deposit-unfreeze-job.ts +++ b/apps/backend/src/services/scheduler/deposit-unfreeze-job.ts @@ -1,6 +1,6 @@ import type { JsonObject } from '../../types/json.js' import { logInfo } from '../../utils/logger.js' -import { settleDueDepositUnfreezes } from '../worker-platform/worker-service.js' +import { settleDueDepositUnfreezes } from '../worker-platform/worker-profile-service.js' export async function runDepositUnfreezeJob(job: JsonObject) { const config = diff --git a/apps/backend/src/services/worker-platform/index.ts b/apps/backend/src/services/worker-platform/index.ts index 3901fcbc..5f8358eb 100644 --- a/apps/backend/src/services/worker-platform/index.ts +++ b/apps/backend/src/services/worker-platform/index.ts @@ -8,6 +8,7 @@ export * from './worker-credentials-service.js' export * from './worker-sms-service.js' export * from './worker-registration-service.js' export * from './worker-login-service.js' +export * from './worker-profile-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-profile-service.ts b/apps/backend/src/services/worker-platform/worker-profile-service.ts new file mode 100644 index 00000000..c88f71b8 --- /dev/null +++ b/apps/backend/src/services/worker-platform/worker-profile-service.ts @@ -0,0 +1,547 @@ +import { runtimeConfig } from '../../config/runtime.js' +import { + countWorkerAcceptedOrders, + countWorkerActiveOrders, + countWorkerTimeoutEvents, + countWorkerWithdrawRequestsOnDay, + createWorkerFinanceRequest, + createWorkerWithdrawalAccount as createWorkerWithdrawalAccountRecord, + getWorkerFinanceRequestSummary, + getWorkerWithdrawalAccount, + listDueDepositUnfreezes, + listWorkerFinanceRequests, + listWorkerLevels, + listWorkerWalletLedgers, + listWorkerWithdrawalAccounts, + releaseDepositUnfreeze, + upsertWorkerWithdrawalAccount, + type WorkerUserRow, +} 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 { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js' +import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js' +import { + createWorkerRechargeAdminNotification, + createWorkerWithdrawAdminNotification, +} from '../admin/admin-notification-service.js' +import { + publishWorkerFinanceRealtimeChange, + publishWorkerWalletRealtimeChange, +} from '../realtime/realtime-event-service.js' +import { + assertWithdrawChannelAllowedForAmount, + mapFinanceRequest, + mapWalletLedger, + mapWorkerUser, + normalizeAmountFen, + normalizeFinanceRequestStatus, + normalizeFinanceRequestType, + normalizeInteger, + normalizeProofFiles, + normalizeWalletLedgerType, + normalizeWithdrawChannel, + resolveWorkerPermissions, +} from './mappers.js' +import { getWorkerFinanceConfig } from './worker-finance-config-service.js' +import { + getRequiredWorker, + requireActiveWorkerSession, + type WorkerSession, +} from './worker-session-context-service.js' + +const WORKER_DAILY_WITHDRAW_LIMIT = 3 + +export async function getWorkerProfile(session: WorkerSession) { + const worker = await getRequiredWorker(session.workerId) + const [ + financeSummary, + acceptedOrderCount, + activeOrderCount, + timeoutOrderCount, + financeConfig, + withdrawalAccounts, + ] = await Promise.all([ + getWorkerFinanceRequestSummary(session.workerId), + countWorkerAcceptedOrders(session.workerId), + countWorkerActiveOrders(session.workerId), + countWorkerTimeoutEvents(session.workerId), + Promise.resolve(getWorkerFinanceConfig()), + listWorkerWithdrawalAccounts(session.workerId), + ]) + const permissions = resolveWorkerPermissions(worker) + const levelProgress = await resolveLevelProgress(worker, acceptedOrderCount) + return { + worker: mapWorkerUser(worker), + permissions, + summary: { + acceptedOrderCount, + activeOrderCount, + timeoutOrderCount, + pendingWithdrawAmount: financeSummary.pendingWithdrawAmount, + approvedWithdrawAmount: financeSummary.approvedWithdrawAmount, + pendingRechargeAmount: financeSummary.pendingRechargeAmount, + }, + levelProgress, + financeConfig, + withdrawalAccounts: withdrawalAccounts.map(mapWithdrawalAccount), + orderTimelineVisible: runtimeConfig.worker.orderTimelineVisible === true, + } +} + +async function resolveLevelProgress( + worker: WorkerUserRow, + acceptedOrderCount: number, +): Promise<{ + currentThreshold: number + nextThreshold: number | null + progressPercent: number +} | null> { + if (!worker.level_id) return null + const levels = await listWorkerLevels() + const active = levels.filter((level) => level.status === 'active') + const current = active.find((level) => Number(level.id) === Number(worker.level_id)) + if (!current) return null + const currentThreshold = normalizeInteger( + safeParseJson(current.permission_json).upgradeThreshold, + 0, + ) + const next = active + .filter( + (level) => + normalizeInteger(safeParseJson(level.permission_json).upgradeThreshold, 0) > + currentThreshold, + ) + .sort( + (a, b) => + normalizeInteger(safeParseJson(a.permission_json).upgradeThreshold, 0) - + normalizeInteger(safeParseJson(b.permission_json).upgradeThreshold, 0), + )[0] + const nextThreshold = next + ? normalizeInteger(safeParseJson(next.permission_json).upgradeThreshold, 0) + : null + return { + currentThreshold, + nextThreshold, + progressPercent: + nextThreshold && nextThreshold > currentThreshold + ? Math.min( + 100, + Math.round( + ((acceptedOrderCount - currentThreshold) / (nextThreshold - currentThreshold)) * 100, + ), + ) + : 100, + } +} + +export async function listWorkerProfileWalletLedgers( + query: JsonObject = {}, + session: WorkerSession, +) { + const page = normalizePage(query.page) + const pageSize = normalizePageSize(query.pageSize) + const ledgerType = normalizeWalletLedgerType(query.ledgerType) + const { items, total } = await listWorkerWalletLedgers({ + page, + pageSize, + workerId: session.workerId, + ledgerType, + }) + return { + items: items.map(mapWalletLedger), + pagination: { page, pageSize, total }, + } +} + +export async function listWorkerProfileFinanceRequests( + query: JsonObject = {}, + session: WorkerSession, +) { + const page = normalizePage(query.page) + const pageSize = normalizePageSize(query.pageSize) + const status = normalizeFinanceRequestStatus(query.status) + const requestType = normalizeFinanceRequestType(query.requestType) + const { items, total } = await listWorkerFinanceRequests({ + page, + pageSize, + workerId: session.workerId, + status, + requestType, + }) + return { + items: items.map(mapFinanceRequest), + pagination: { page, pageSize, total }, + } +} + +export async function createWorkerRechargeRequest( + payload: JsonObject = {}, + session: WorkerSession, +) { + requireActiveWorkerSession(session) + const worker = await getRequiredWorker(session.workerId) + const financeConfig = getWorkerFinanceConfig() + if (financeConfig.recharge.enabled === false) { + throw createHttpError('当前充值入口暂未开放,请联系管理员', { + statusCode: 409, + errorCode: 'worker_recharge_disabled', + }) + } + const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0) + if (amount <= 0) { + throw createHttpError('充值金额必须大于 0', { + statusCode: 400, + errorCode: 'worker_recharge_amount_invalid', + }) + } + + const note = String(payload.note || payload.remark || '').trim() + const proofFiles = normalizeProofFiles(payload.proofFiles || payload.proofFilesList) + const paidAt = String(payload.paidAt || '').trim() + const created = await createWorkerFinanceRequest({ + workerId: worker.id, + requestType: 'recharge', + amount, + accountChannel: 'manual', + accountName: worker.display_name || worker.username, + accountNo: worker.phone || '', + note: note || '个人中心充值申请', + payloadJson: JSON.stringify({ + source: 'worker_profile_recharge', + username: worker.username, + phone: worker.phone || '', + paidAt: paidAt || null, + proofFiles, + financeConfig: { + channelName: financeConfig.recharge.channelName, + accountName: financeConfig.recharge.accountName, + accountNo: financeConfig.recharge.accountNo, + qrCodeImage: financeConfig.recharge.qrCodeImage, + }, + }), + now: nowIso(), + }) + if (!created) { + throw createHttpError('充值申请创建失败', { + statusCode: 500, + errorCode: 'worker_recharge_request_create_failed', + }) + } + await createWorkerRechargeAdminNotification({ + requestId: Number(created.id), + workerName: worker.display_name || worker.username, + amountFen: Number(created.amount || amount), + }) + publishWorkerFinanceRealtimeChange({ + requestId: Number(created.id), + workerId: Number(worker.id), + }) + return { + request: mapFinanceRequest(created), + } +} + +export async function createWorkerWithdrawRequest( + payload: JsonObject = {}, + session: WorkerSession, +) { + requireActiveWorkerSession(session) + const worker = await getRequiredWorker(session.workerId) + const financeConfig = getWorkerFinanceConfig() + if (financeConfig.withdraw.enabled === false) { + throw createHttpError('当前提现入口暂未开放,请联系管理员', { + statusCode: 409, + errorCode: 'worker_withdraw_disabled', + }) + } + + await settleDueDepositUnfreezes({ workerId: worker.id, limit: 50 }) + const dayRange = resolveChinaDayRange() + const withdrawCountToday = await countWorkerWithdrawRequestsOnDay( + worker.id, + dayRange.start, + dayRange.end, + ) + if (withdrawCountToday >= WORKER_DAILY_WITHDRAW_LIMIT) { + throw createHttpError('每天最多提现 3 次(支付宝、微信共享次数),今天已达上限', { + statusCode: 409, + errorCode: 'worker_withdraw_daily_limit_reached', + }) + } + + const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0) + if (amount <= 0) { + throw createHttpError('提现金额必须大于 0', { + statusCode: 400, + errorCode: 'worker_withdraw_amount_invalid', + }) + } + + const accountChannel = normalizeWithdrawChannel(payload.accountChannel || payload.channel) + const withdrawalAccount = await getWorkerWithdrawalAccount(worker.id, accountChannel) + if (!withdrawalAccount) { + throw createHttpError('请先添加对应的提现信息;修改提现信息请联系客服处理', { + statusCode: 409, + errorCode: 'worker_withdrawal_account_required', + }) + } + assertWithdrawChannelAllowedForAmount(amount, accountChannel) + const alipayQrCodeImage = resolveAlipayWithdrawalQrCode(withdrawalAccount) + const wechatQrCodeImage = resolveWechatWithdrawalQrCode(withdrawalAccount) + if (accountChannel === 'alipay' && !alipayQrCodeImage) { + throw createHttpError('支付宝提现信息缺少收款二维码,请先补充后再提现', { + statusCode: 409, + errorCode: 'worker_withdrawal_alipay_qr_required', + }) + } + if (accountChannel === 'wechat' && !wechatQrCodeImage) { + throw createHttpError('微信提现信息缺少收款二维码,请联系客服处理', { + statusCode: 409, + errorCode: 'worker_withdrawal_wechat_qr_required', + }) + } + + const financeSummary = await getWorkerFinanceRequestSummary(worker.id) + const availableAmount = Number(worker.available_amount || 0) + const availableForWithdraw = Math.max(0, availableAmount - financeSummary.pendingWithdrawAmount) + if (amount > availableForWithdraw) { + throw createHttpError('可提现余额不足,请先减少提现金额或等待已提交申请处理', { + statusCode: 409, + errorCode: 'worker_withdraw_amount_insufficient', + }) + } + + const note = String(payload.note || payload.remark || '').trim() + const created = await createWorkerFinanceRequest({ + workerId: worker.id, + requestType: 'withdraw', + amount, + accountChannel, + accountName: withdrawalAccount.account_name, + accountNo: withdrawalAccount.account_no, + note: note || '个人中心提现申请', + payloadJson: JSON.stringify({ + source: 'worker_profile_withdraw', + username: worker.username, + phone: worker.phone || '', + alipayQrCodeImage, + wechatQrCodeImage, + }), + now: nowIso(), + }) + if (!created) { + throw createHttpError('提现申请创建失败', { + statusCode: 500, + errorCode: 'worker_withdraw_request_create_failed', + }) + } + await createWorkerWithdrawAdminNotification({ + requestId: Number(created.id), + workerName: worker.display_name || worker.username, + amountFen: Number(created.amount || amount), + channel: created.account_channel, + }) + publishWorkerFinanceRealtimeChange({ + requestId: Number(created.id), + workerId: Number(worker.id), + }) + return { + request: mapFinanceRequest(created), + availableForWithdraw, + } +} + +export async function createWorkerWithdrawalAccount( + payload: JsonObject = {}, + session: WorkerSession, +) { + requireActiveWorkerSession(session) + const worker = await getRequiredWorker(session.workerId) + const accountChannel = normalizeWithdrawChannel(payload.accountChannel || payload.channel) + const existing = await getWorkerWithdrawalAccount(worker.id, accountChannel) + if (existing) { + throw createHttpError('该提现方式的信息已添加,修改请联系客服处理', { + statusCode: 409, + errorCode: 'worker_withdrawal_account_already_set', + }) + } + + const accountName = String(payload.accountName || payload.realName || '').trim() + const accountNo = + accountChannel === 'alipay' ? String(payload.accountNo || payload.account || '').trim() : '' + const alipayQrCodeImage = + accountChannel === 'alipay' + ? normalizeProofFiles(payload.alipayQrCodeImages || payload.alipayQrCodes)[0] || null + : null + const wechatQrCodeImage = + accountChannel === 'wechat' + ? normalizeProofFiles(payload.wechatQrCodeImages || payload.wechatQrCodes)[0] || null + : null + if (!accountName) { + throw createHttpError('请填写收款人姓名', { + statusCode: 400, + errorCode: 'worker_withdraw_account_name_required', + }) + } + if (accountChannel === 'alipay' && !accountNo) { + throw createHttpError('请填写收款账号', { + statusCode: 400, + errorCode: 'worker_withdraw_account_no_required', + }) + } + if (accountChannel === 'alipay' && !alipayQrCodeImage) { + throw createHttpError('请上传支付宝收款二维码', { + statusCode: 400, + errorCode: 'worker_withdraw_alipay_qr_required', + }) + } + if (accountChannel === 'wechat' && !wechatQrCodeImage) { + throw createHttpError('请上传微信收款二维码', { + statusCode: 400, + errorCode: 'worker_withdraw_wechat_qr_required', + }) + } + + const created = await createWorkerWithdrawalAccountRecord({ + workerId: worker.id, + accountChannel, + accountName, + accountNo, + alipayQrCodeJson: JSON.stringify(alipayQrCodeImage || {}), + wechatQrCodeJson: JSON.stringify(wechatQrCodeImage || {}), + now: nowIso(), + }) + if (!created) { + throw createHttpError('该提现方式的信息已添加,修改请联系客服处理', { + statusCode: 409, + errorCode: 'worker_withdrawal_account_already_set', + }) + } + return { + withdrawalAccount: mapWithdrawalAccount(created), + } +} + +/** 历史支付宝账户只允许补充一次收款二维码,不允许修改账户姓名、账号或二维码。 */ +export async function saveWorkerAlipayWithdrawalQrCode( + payload: JsonObject = {}, + session: WorkerSession, +) { + requireActiveWorkerSession(session) + const worker = await getRequiredWorker(session.workerId) + const current = await getWorkerWithdrawalAccount(worker.id, 'alipay') + if (!current) { + throw createHttpError('请先添加支付宝提现信息', { + statusCode: 409, + errorCode: 'worker_withdrawal_alipay_account_required', + }) + } + if (resolveAlipayWithdrawalQrCode(current)) { + throw createHttpError('支付宝收款二维码已补充,后续修改请联系客服处理', { + statusCode: 409, + errorCode: 'worker_withdrawal_alipay_qr_already_set', + }) + } + const alipayQrCodeImage = normalizeProofFiles( + payload.alipayQrCodeImages || payload.alipayQrCodes, + )[0] + if (!alipayQrCodeImage) { + throw createHttpError('请上传支付宝收款二维码', { + statusCode: 400, + errorCode: 'worker_withdraw_alipay_qr_required', + }) + } + const saved = await upsertWorkerWithdrawalAccount({ + workerId: worker.id, + accountChannel: 'alipay', + accountName: current.account_name, + accountNo: current.account_no, + alipayQrCodeJson: JSON.stringify(alipayQrCodeImage), + wechatQrCodeJson: JSON.stringify(safeParseJson(current.wechat_qr_code_json)), + now: nowIso(), + }) + if (!saved) { + throw createHttpError('保存支付宝收款二维码失败', { + statusCode: 500, + errorCode: 'worker_withdrawal_alipay_qr_save_failed', + }) + } + return { withdrawalAccount: mapWithdrawalAccount(saved) } +} + +export async function settleDueDepositUnfreezes( + options: { limit?: number; workerId?: number } = {}, +) { + const due = await listDueDepositUnfreezes({ + limit: Math.max(1, Number(options.limit || 100)), + workerId: Number(options.workerId || 0), + }) + let processedCount = 0 + for (const unfreeze of due) { + const released = await releaseDepositUnfreeze({ + unfreezeId: unfreeze.id, + now: nowIso(), + }) + if (released) { + processedCount += 1 + publishWorkerWalletRealtimeChange(Number(released.worker_id)) + } + } + return { + checkedCount: due.length, + processedCount, + } +} + +function mapWithdrawalAccount(account: { + account_channel: string + account_name: string + account_no: string + alipay_qr_code_json: string | Record + wechat_qr_code_json: string | Record + created_at: string +}) { + const alipayQrCodeImage = resolveAlipayWithdrawalQrCode(account) + const wechatQrCodeImage = resolveWechatWithdrawalQrCode(account) + return { + accountChannel: account.account_channel, + accountName: account.account_name, + accountNoMasked: + account.account_channel === 'alipay' ? maskWithdrawalAccountNo(account.account_no) : '', + alipayQrCodeImage, + wechatQrCodeImage, + createdAt: account.created_at, + } +} + +function resolveAlipayWithdrawalQrCode(account: { + alipay_qr_code_json: string | Record +}) { + const file = refreshUploadedFileUrls(safeParseJson(account.alipay_qr_code_json)) + return String(file.url || '').trim() ? file : null +} + +function resolveWechatWithdrawalQrCode(account: { + wechat_qr_code_json: string | Record +}) { + const file = refreshUploadedFileUrls(safeParseJson(account.wechat_qr_code_json)) + return String(file.url || '').trim() ? file : null +} + +function maskWithdrawalAccountNo(accountNo: string) { + const normalized = String(accountNo || '').trim() + if (normalized.length <= 4) return normalized + return `${'*'.repeat(Math.max(4, normalized.length - 4))}${normalized.slice(-4)}` +} + +function resolveChinaDayRange(): { start: string; end: string } { + const utcNow = new Date() + const utcDayStartMs = + Date.UTC(utcNow.getUTCFullYear(), utcNow.getUTCMonth(), utcNow.getUTCDate()) - 8 * 3_600_000 + return { + start: new Date(utcDayStartMs).toISOString(), + end: new Date(utcDayStartMs + 86_400_000).toISOString(), + } +} diff --git a/apps/backend/src/services/worker-platform/worker-service.ts b/apps/backend/src/services/worker-platform/worker-service.ts index 5aae8eb7..c438a5dd 100644 --- a/apps/backend/src/services/worker-platform/worker-service.ts +++ b/apps/backend/src/services/worker-platform/worker-service.ts @@ -3,22 +3,15 @@ import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js' import { acceptWorkOrderAndSettle, acceptWorkOrderShareAndSettle, - countWorkerAcceptedOrders, createWorkerCancelRequest, createWorkerOrderFeedback, countWorkerActiveOrders, - countWorkerTimeoutEvents, - countWorkerWithdrawRequestsOnDay, countWorkOrderPendingSharingSubmissions, consumeWorkerSmsCode, createWorkOrderEvent, - createWorkerFinanceRequest, - createWorkerWithdrawalAccount as createWorkerWithdrawalAccountRecord, - getWorkerFinanceRequestSummary, getLatestWorkerSmsCode, recordWorkerSmsCodeFailure, getWorkerUserById, - getWorkerWithdrawalAccount, getWorkerUserByPhone, getWorkerUserByUsername, getWorkerUserByDisplayName, @@ -33,26 +26,19 @@ import { touchWorkerSession, joinWorkOrderShare, listPendingMaterialWorkOrdersByPlatformOrderId, - listWorkerFinanceRequests, listWorkerCompletionLeaderboard, listWorkerCancelRequests, listWorkerOrderFeedbacks, listWorkerSharesByWorker, - listWorkerWalletLedgers, listWorkerWorkOrderNotes, listWorkerWorkOrderViews, - listWorkerWithdrawalAccounts, listWorkCategories, listWorkOrders, listWorkOrderEventsByOrderId, listWorkOrderShares, listWorkOrderSharesByOrderIds, - listWorkerLevels, - upsertWorkerWithdrawalAccount, getWorkerCancelRequestRisk, - listDueDepositUnfreezes, listOverdueWorkOrders, - releaseDepositUnfreeze, settleOverdueWorkOrder, submitWorkOrderShareAcceptance, updateWorkOrder, @@ -65,14 +51,13 @@ import { type WorkOrderRow, type WorkOrderShareRow, type WorkerSessionRow, - type WorkerUserRow, } from '../../repositories/worker-platform/index.js' import type { JsonObject } from '../../types/json.js' import { createHttpError } from '../../utils/http.js' import { addHours, nowIso } from '../../utils/time.js' import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js' -import { getWorkerFinanceConfig } from './worker-finance-config-service.js' import { getWorkerAnnouncementConfig } from './worker-announcement-config-service.js' +import { getWorkerFinanceConfig } from './worker-finance-config-service.js' import { getWorkerHallConfig } from './worker-hall-config-service.js' import { getRequiredWorkOrder, @@ -80,29 +65,20 @@ import { requireActiveWorkerSession, type WorkerSession, } from './worker-session-context-service.js' -import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js' import { createWorkerAcceptanceAdminNotification, createWorkerCancelRequestAdminNotification, createWorkerOrderFeedbackAdminNotification, - createWorkerRechargeAdminNotification, - createWorkerWithdrawAdminNotification, getWorkerAcceptanceReminder, remindWorkerAcceptanceAdminNotification, } from '../admin/admin-notification-service.js' import { - publishWorkerFinanceRealtimeChange, publishWorkerWalletRealtimeChange, publishWorkOrderRealtimeChange, } from '../realtime/realtime-event-service.js' import { - createWorkerSession, - ensureWorkerAuthConfigured, - hashWorkerPassword, mapCollectLookupOrder, - mapFinanceRequest, - mapWalletLedger, mapWorkCategory, mapWorkOrderForWorker, mapWorkOrderPublic, @@ -110,33 +86,18 @@ import { mapWorkOrderShare, mapWorkerCancelRequest, mapWorkerOrderFeedback, - mapWorkerUser, - normalizeAmountFen, - assertWithdrawChannelAllowedForAmount, normalizeBoolean, - normalizeFinanceRequestStatus, - normalizeFinanceRequestType, - normalizeInteger, normalizeOptionalId, - normalizePassword, normalizePositiveInteger, - normalizeProofFiles, - normalizeSessionVersion, normalizeStatuses, normalizeStringArray, normalizeSubmittedFields, normalizeUploadedFiles, - normalizeUsername, - normalizeWalletLedgerType, - normalizeWithdrawChannel, resolveFreezeDepositAmount, resolveRequirementFields, resolveWorkerPermissions, canWorkerAutoAcceptWholeOrder, throwGrabWorkOrderFailure, - validateWorkerPassword, - isWorkerPasswordStrong, - verifyWorkerPassword, } from './mappers.js' import { assertWorkerLoginAllowed, @@ -153,8 +114,6 @@ import { } from './worker-login-service.js' import { registerWorker, resetWorkerPassword } from './worker-registration-service.js' import { - hashSmsCode, - normalizeWorkerPhone, sendWorkerLoginSmsCode, sendWorkerPasswordResetSmsCode, sendWorkerSmsCode, @@ -169,9 +128,7 @@ import { } from './worker-session-auth-service.js' import { normalizeWorkOrderTimeoutPolicy } from './work-order-timeout-policy.js' -const WORKER_DAILY_WITHDRAW_LIMIT = 3 const VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS = 24 -const WORKER_SMS_MAX_VERIFY_ATTEMPTS = 5 export { getRequiredWorkOrder, @@ -208,463 +165,15 @@ export { verifyWorkerSessionToken, } from './worker-session-auth-service.js' -export async function getWorkerProfile(session: WorkerSession) { - const worker = await getRequiredWorker(session.workerId) - const [ - financeSummary, - acceptedOrderCount, - activeOrderCount, - timeoutOrderCount, - financeConfig, - withdrawalAccounts, - ] = await Promise.all([ - getWorkerFinanceRequestSummary(session.workerId), - countWorkerAcceptedOrders(session.workerId), - countWorkerActiveOrders(session.workerId), - countWorkerTimeoutEvents(session.workerId), - Promise.resolve(getWorkerFinanceConfig()), - listWorkerWithdrawalAccounts(session.workerId), - ]) - const permissions = resolveWorkerPermissions(worker) - const levelProgress = await resolveLevelProgress(worker, acceptedOrderCount) - return { - worker: mapWorkerUser(worker), - permissions, - summary: { - acceptedOrderCount, - activeOrderCount, - timeoutOrderCount, - pendingWithdrawAmount: financeSummary.pendingWithdrawAmount, - approvedWithdrawAmount: financeSummary.approvedWithdrawAmount, - pendingRechargeAmount: financeSummary.pendingRechargeAmount, - }, - levelProgress, - financeConfig, - withdrawalAccounts: withdrawalAccounts.map(mapWithdrawalAccount), - orderTimelineVisible: runtimeConfig.worker.orderTimelineVisible === true, - } -} - -async function resolveLevelProgress( - worker: WorkerUserRow, - acceptedOrderCount: number, -): Promise<{ - currentThreshold: number - nextThreshold: number | null - progressPercent: number -} | null> { - if (!worker.level_id) return null - const levels = await listWorkerLevels() - const active = levels.filter((level) => level.status === 'active') - const current = active.find((level) => Number(level.id) === Number(worker.level_id)) - if (!current) return null - const currentThreshold = normalizeInteger( - safeParseJson(current.permission_json).upgradeThreshold, - 0, - ) - const next = active - .filter( - (level) => - normalizeInteger(safeParseJson(level.permission_json).upgradeThreshold, 0) > - currentThreshold, - ) - .sort( - (a, b) => - normalizeInteger(safeParseJson(a.permission_json).upgradeThreshold, 0) - - normalizeInteger(safeParseJson(b.permission_json).upgradeThreshold, 0), - )[0] - const nextThreshold = next - ? normalizeInteger(safeParseJson(next.permission_json).upgradeThreshold, 0) - : null - return { - currentThreshold, - nextThreshold, - progressPercent: - nextThreshold && nextThreshold > currentThreshold - ? Math.min( - 100, - Math.round( - ((acceptedOrderCount - currentThreshold) / (nextThreshold - currentThreshold)) * 100, - ), - ) - : 100, - } -} - -export async function listWorkerProfileWalletLedgers( - query: JsonObject = {}, - session: WorkerSession, -) { - const page = normalizePage(query.page) - const pageSize = normalizePageSize(query.pageSize) - const ledgerType = normalizeWalletLedgerType(query.ledgerType) - const { items, total } = await listWorkerWalletLedgers({ - page, - pageSize, - workerId: session.workerId, - ledgerType, - }) - return { - items: items.map(mapWalletLedger), - pagination: { page, pageSize, total }, - } -} - -export async function listWorkerProfileFinanceRequests( - query: JsonObject = {}, - session: WorkerSession, -) { - const page = normalizePage(query.page) - const pageSize = normalizePageSize(query.pageSize) - const status = normalizeFinanceRequestStatus(query.status) - const requestType = normalizeFinanceRequestType(query.requestType) - const { items, total } = await listWorkerFinanceRequests({ - page, - pageSize, - workerId: session.workerId, - status, - requestType, - }) - return { - items: items.map(mapFinanceRequest), - pagination: { page, pageSize, total }, - } -} - -export async function createWorkerRechargeRequest( - payload: JsonObject = {}, - session: WorkerSession, -) { - requireActiveWorkerSession(session) - const worker = await getRequiredWorker(session.workerId) - const financeConfig = getWorkerFinanceConfig() - if (financeConfig.recharge.enabled === false) { - throw createHttpError('当前充值入口暂未开放,请联系管理员', { - statusCode: 409, - errorCode: 'worker_recharge_disabled', - }) - } - const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0) - if (amount <= 0) { - throw createHttpError('充值金额必须大于 0', { - statusCode: 400, - errorCode: 'worker_recharge_amount_invalid', - }) - } - - const note = String(payload.note || payload.remark || '').trim() - const proofFiles = normalizeProofFiles(payload.proofFiles || payload.proofFilesList) - const paidAt = String(payload.paidAt || '').trim() - const created = await createWorkerFinanceRequest({ - workerId: worker.id, - requestType: 'recharge', - amount, - accountChannel: 'manual', - accountName: worker.display_name || worker.username, - accountNo: worker.phone || '', - note: note || '个人中心充值申请', - payloadJson: JSON.stringify({ - source: 'worker_profile_recharge', - username: worker.username, - phone: worker.phone || '', - paidAt: paidAt || null, - proofFiles, - financeConfig: { - channelName: financeConfig.recharge.channelName, - accountName: financeConfig.recharge.accountName, - accountNo: financeConfig.recharge.accountNo, - qrCodeImage: financeConfig.recharge.qrCodeImage, - }, - }), - now: nowIso(), - }) - if (!created) { - throw createHttpError('充值申请创建失败', { - statusCode: 500, - errorCode: 'worker_recharge_request_create_failed', - }) - } - await createWorkerRechargeAdminNotification({ - requestId: Number(created.id), - workerName: worker.display_name || worker.username, - amountFen: Number(created.amount || amount), - }) - publishWorkerFinanceRealtimeChange({ - requestId: Number(created.id), - workerId: Number(worker.id), - }) - return { - request: mapFinanceRequest(created), - } -} - -export async function createWorkerWithdrawRequest( - payload: JsonObject = {}, - session: WorkerSession, -) { - requireActiveWorkerSession(session) - const worker = await getRequiredWorker(session.workerId) - const financeConfig = getWorkerFinanceConfig() - if (financeConfig.withdraw.enabled === false) { - throw createHttpError('当前提现入口暂未开放,请联系管理员', { - statusCode: 409, - errorCode: 'worker_withdraw_disabled', - }) - } - - await settleDueDepositUnfreezes({ workerId: worker.id, limit: 50 }) - const withdrawCountToday = await countWorkerWithdrawRequestsOnDay( - worker.id, - resolveChinaDayRange().start, - resolveChinaDayRange().end, - ) - if (withdrawCountToday >= WORKER_DAILY_WITHDRAW_LIMIT) { - throw createHttpError('每天最多提现 3 次(支付宝、微信共享次数),今天已达上限', { - statusCode: 409, - errorCode: 'worker_withdraw_daily_limit_reached', - }) - } - - const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0) - if (amount <= 0) { - throw createHttpError('提现金额必须大于 0', { - statusCode: 400, - errorCode: 'worker_withdraw_amount_invalid', - }) - } - - const accountChannel = normalizeWithdrawChannel(payload.accountChannel || payload.channel) - const withdrawalAccount = await getWorkerWithdrawalAccount(worker.id, accountChannel) - if (!withdrawalAccount) { - throw createHttpError('请先添加对应的提现信息;修改提现信息请联系客服处理', { - statusCode: 409, - errorCode: 'worker_withdrawal_account_required', - }) - } - assertWithdrawChannelAllowedForAmount(amount, accountChannel) - const alipayQrCodeImage = resolveAlipayWithdrawalQrCode(withdrawalAccount) - const wechatQrCodeImage = resolveWechatWithdrawalQrCode(withdrawalAccount) - if (accountChannel === 'alipay' && !alipayQrCodeImage) { - throw createHttpError('支付宝提现信息缺少收款二维码,请先补充后再提现', { - statusCode: 409, - errorCode: 'worker_withdrawal_alipay_qr_required', - }) - } - if (accountChannel === 'wechat' && !wechatQrCodeImage) { - throw createHttpError('微信提现信息缺少收款二维码,请联系客服处理', { - statusCode: 409, - errorCode: 'worker_withdrawal_wechat_qr_required', - }) - } - - const financeSummary = await getWorkerFinanceRequestSummary(worker.id) - const availableAmount = Number(worker.available_amount || 0) - const availableForWithdraw = Math.max(0, availableAmount - financeSummary.pendingWithdrawAmount) - if (amount > availableForWithdraw) { - throw createHttpError('可提现余额不足,请先减少提现金额或等待已提交申请处理', { - statusCode: 409, - errorCode: 'worker_withdraw_amount_insufficient', - }) - } - - const note = String(payload.note || payload.remark || '').trim() - const created = await createWorkerFinanceRequest({ - workerId: worker.id, - requestType: 'withdraw', - amount, - accountChannel, - accountName: withdrawalAccount.account_name, - accountNo: withdrawalAccount.account_no, - note: note || '个人中心提现申请', - payloadJson: JSON.stringify({ - source: 'worker_profile_withdraw', - username: worker.username, - phone: worker.phone || '', - alipayQrCodeImage, - wechatQrCodeImage, - }), - now: nowIso(), - }) - if (!created) { - throw createHttpError('提现申请创建失败', { - statusCode: 500, - errorCode: 'worker_withdraw_request_create_failed', - }) - } - await createWorkerWithdrawAdminNotification({ - requestId: Number(created.id), - workerName: worker.display_name || worker.username, - amountFen: Number(created.amount || amount), - channel: created.account_channel, - }) - publishWorkerFinanceRealtimeChange({ - requestId: Number(created.id), - workerId: Number(worker.id), - }) - return { - request: mapFinanceRequest(created), - availableForWithdraw, - } -} - -export async function createWorkerWithdrawalAccount( - payload: JsonObject = {}, - session: WorkerSession, -) { - requireActiveWorkerSession(session) - const worker = await getRequiredWorker(session.workerId) - const accountChannel = normalizeWithdrawChannel(payload.accountChannel || payload.channel) - const existing = await getWorkerWithdrawalAccount(worker.id, accountChannel) - if (existing) { - throw createHttpError('该提现方式的信息已添加,修改请联系客服处理', { - statusCode: 409, - errorCode: 'worker_withdrawal_account_already_set', - }) - } - - const accountName = String(payload.accountName || payload.realName || '').trim() - const accountNo = - accountChannel === 'alipay' ? String(payload.accountNo || payload.account || '').trim() : '' - const alipayQrCodeImage = - accountChannel === 'alipay' - ? normalizeProofFiles(payload.alipayQrCodeImages || payload.alipayQrCodes)[0] || null - : null - const wechatQrCodeImage = - accountChannel === 'wechat' - ? normalizeProofFiles(payload.wechatQrCodeImages || payload.wechatQrCodes)[0] || null - : null - if (!accountName) { - throw createHttpError('请填写收款人姓名', { - statusCode: 400, - errorCode: 'worker_withdraw_account_name_required', - }) - } - if (accountChannel === 'alipay' && !accountNo) { - throw createHttpError('请填写收款账号', { - statusCode: 400, - errorCode: 'worker_withdraw_account_no_required', - }) - } - if (accountChannel === 'alipay' && !alipayQrCodeImage) { - throw createHttpError('请上传支付宝收款二维码', { - statusCode: 400, - errorCode: 'worker_withdraw_alipay_qr_required', - }) - } - if (accountChannel === 'wechat' && !wechatQrCodeImage) { - throw createHttpError('请上传微信收款二维码', { - statusCode: 400, - errorCode: 'worker_withdraw_wechat_qr_required', - }) - } - - const created = await createWorkerWithdrawalAccountRecord({ - workerId: worker.id, - accountChannel, - accountName, - accountNo, - alipayQrCodeJson: JSON.stringify(alipayQrCodeImage || {}), - wechatQrCodeJson: JSON.stringify(wechatQrCodeImage || {}), - now: nowIso(), - }) - if (!created) { - throw createHttpError('该提现方式的信息已添加,修改请联系客服处理', { - statusCode: 409, - errorCode: 'worker_withdrawal_account_already_set', - }) - } - return { - withdrawalAccount: mapWithdrawalAccount(created), - } -} - -/** 历史支付宝账户只允许补充一次收款二维码,不允许修改账户姓名、账号或二维码。 */ -export async function saveWorkerAlipayWithdrawalQrCode( - payload: JsonObject = {}, - session: WorkerSession, -) { - requireActiveWorkerSession(session) - const worker = await getRequiredWorker(session.workerId) - const current = await getWorkerWithdrawalAccount(worker.id, 'alipay') - if (!current) { - throw createHttpError('请先添加支付宝提现信息', { - statusCode: 409, - errorCode: 'worker_withdrawal_alipay_account_required', - }) - } - if (resolveAlipayWithdrawalQrCode(current)) { - throw createHttpError('支付宝收款二维码已补充,后续修改请联系客服处理', { - statusCode: 409, - errorCode: 'worker_withdrawal_alipay_qr_already_set', - }) - } - const alipayQrCodeImage = normalizeProofFiles( - payload.alipayQrCodeImages || payload.alipayQrCodes, - )[0] - if (!alipayQrCodeImage) { - throw createHttpError('请上传支付宝收款二维码', { - statusCode: 400, - errorCode: 'worker_withdraw_alipay_qr_required', - }) - } - const saved = await upsertWorkerWithdrawalAccount({ - workerId: worker.id, - accountChannel: 'alipay', - accountName: current.account_name, - accountNo: current.account_no, - alipayQrCodeJson: JSON.stringify(alipayQrCodeImage), - wechatQrCodeJson: JSON.stringify(safeParseJson(current.wechat_qr_code_json)), - now: nowIso(), - }) - if (!saved) { - throw createHttpError('保存支付宝收款二维码失败', { - statusCode: 500, - errorCode: 'worker_withdrawal_alipay_qr_save_failed', - }) - } - return { withdrawalAccount: mapWithdrawalAccount(saved) } -} - -function mapWithdrawalAccount(account: { - account_channel: string - account_name: string - account_no: string - alipay_qr_code_json: string | Record - wechat_qr_code_json: string | Record - created_at: string -}) { - const alipayQrCodeImage = resolveAlipayWithdrawalQrCode(account) - const wechatQrCodeImage = resolveWechatWithdrawalQrCode(account) - return { - accountChannel: account.account_channel, - accountName: account.account_name, - accountNoMasked: - account.account_channel === 'alipay' ? maskWithdrawalAccountNo(account.account_no) : '', - alipayQrCodeImage, - wechatQrCodeImage, - createdAt: account.created_at, - } -} - -function resolveAlipayWithdrawalQrCode(account: { - alipay_qr_code_json: string | Record -}) { - const file = refreshUploadedFileUrls(safeParseJson(account.alipay_qr_code_json)) - return String(file.url || '').trim() ? file : null -} - -function resolveWechatWithdrawalQrCode(account: { - wechat_qr_code_json: string | Record -}) { - const file = refreshUploadedFileUrls(safeParseJson(account.wechat_qr_code_json)) - return String(file.url || '').trim() ? file : null -} - -function maskWithdrawalAccountNo(accountNo: string) { - const normalized = String(accountNo || '').trim() - if (normalized.length <= 4) return normalized - return `${'*'.repeat(Math.max(4, normalized.length - 4))}${normalized.slice(-4)}` -} +export { + createWorkerRechargeRequest, + createWorkerWithdrawalAccount, + createWorkerWithdrawRequest, + getWorkerProfile, + listWorkerProfileFinanceRequests, + listWorkerProfileWalletLedgers, + saveWorkerAlipayWithdrawalQrCode, +} from './worker-profile-service.js' export async function listWorkerHallOrders(query: JsonObject = {}, session: WorkerSession) { requireActiveWorkerSession(session) @@ -858,39 +367,7 @@ function resolveWorkOrderDeadlineAt(workOrder: WorkOrderRow, now: string): strin return new Date(new Date(now).getTime() + timeoutMinutes * 60 * 1000).toISOString() } -export async function settleDueDepositUnfreezes( - options: { limit?: number; workerId?: number } = {}, -) { - const due = await listDueDepositUnfreezes({ - limit: Math.max(1, Number(options.limit || 100)), - workerId: Number(options.workerId || 0), - }) - let processedCount = 0 - for (const unfreeze of due) { - const released = await releaseDepositUnfreeze({ - unfreezeId: unfreeze.id, - now: nowIso(), - }) - if (released) { - processedCount += 1 - publishWorkerWalletRealtimeChange(Number(released.worker_id)) - } - } - return { - checkedCount: due.length, - processedCount, - } -} - -function resolveChinaDayRange(): { start: string; end: string } { - const utcNow = new Date() - const utcDayStartMs = - Date.UTC(utcNow.getUTCFullYear(), utcNow.getUTCMonth(), utcNow.getUTCDate()) - 8 * 3_600_000 - return { - start: new Date(utcDayStartMs).toISOString(), - end: new Date(utcDayStartMs + 86_400_000).toISOString(), - } -} +export { settleDueDepositUnfreezes } from './worker-profile-service.js' export async function settleOverdueWorkOrders(options: { limit?: number; workerId?: number } = {}) { const overdue = await listOverdueWorkOrders({