拆分打手资料资金服务

This commit is contained in:
yml2213
2026-08-21 16:48:52 +08:00
parent 27e9a574e4
commit 8388d6e875
4 changed files with 560 additions and 535 deletions
@@ -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 =
@@ -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'
@@ -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<string, unknown>
wechat_qr_code_json: string | Record<string, unknown>
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<string, unknown>
}) {
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<string, unknown>
}) {
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(),
}
}
@@ -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<string, unknown>
wechat_qr_code_json: string | Record<string, unknown>
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<string, unknown>
}) {
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<string, unknown>
}) {
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({