拆分打手资料资金服务
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import type { JsonObject } from '../../types/json.js'
|
import type { JsonObject } from '../../types/json.js'
|
||||||
import { logInfo } from '../../utils/logger.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) {
|
export async function runDepositUnfreezeJob(job: JsonObject) {
|
||||||
const config =
|
const config =
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export * from './worker-credentials-service.js'
|
|||||||
export * from './worker-sms-service.js'
|
export * from './worker-sms-service.js'
|
||||||
export * from './worker-registration-service.js'
|
export * from './worker-registration-service.js'
|
||||||
export * from './worker-login-service.js'
|
export * from './worker-login-service.js'
|
||||||
|
export * from './worker-profile-service.js'
|
||||||
export * from './worker-platform-defaults.js'
|
export * from './worker-platform-defaults.js'
|
||||||
export * from './work-order-timeout-policy.js'
|
export * from './work-order-timeout-policy.js'
|
||||||
export * from './after-sales-service.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 {
|
import {
|
||||||
acceptWorkOrderAndSettle,
|
acceptWorkOrderAndSettle,
|
||||||
acceptWorkOrderShareAndSettle,
|
acceptWorkOrderShareAndSettle,
|
||||||
countWorkerAcceptedOrders,
|
|
||||||
createWorkerCancelRequest,
|
createWorkerCancelRequest,
|
||||||
createWorkerOrderFeedback,
|
createWorkerOrderFeedback,
|
||||||
countWorkerActiveOrders,
|
countWorkerActiveOrders,
|
||||||
countWorkerTimeoutEvents,
|
|
||||||
countWorkerWithdrawRequestsOnDay,
|
|
||||||
countWorkOrderPendingSharingSubmissions,
|
countWorkOrderPendingSharingSubmissions,
|
||||||
consumeWorkerSmsCode,
|
consumeWorkerSmsCode,
|
||||||
createWorkOrderEvent,
|
createWorkOrderEvent,
|
||||||
createWorkerFinanceRequest,
|
|
||||||
createWorkerWithdrawalAccount as createWorkerWithdrawalAccountRecord,
|
|
||||||
getWorkerFinanceRequestSummary,
|
|
||||||
getLatestWorkerSmsCode,
|
getLatestWorkerSmsCode,
|
||||||
recordWorkerSmsCodeFailure,
|
recordWorkerSmsCodeFailure,
|
||||||
getWorkerUserById,
|
getWorkerUserById,
|
||||||
getWorkerWithdrawalAccount,
|
|
||||||
getWorkerUserByPhone,
|
getWorkerUserByPhone,
|
||||||
getWorkerUserByUsername,
|
getWorkerUserByUsername,
|
||||||
getWorkerUserByDisplayName,
|
getWorkerUserByDisplayName,
|
||||||
@@ -33,26 +26,19 @@ import {
|
|||||||
touchWorkerSession,
|
touchWorkerSession,
|
||||||
joinWorkOrderShare,
|
joinWorkOrderShare,
|
||||||
listPendingMaterialWorkOrdersByPlatformOrderId,
|
listPendingMaterialWorkOrdersByPlatformOrderId,
|
||||||
listWorkerFinanceRequests,
|
|
||||||
listWorkerCompletionLeaderboard,
|
listWorkerCompletionLeaderboard,
|
||||||
listWorkerCancelRequests,
|
listWorkerCancelRequests,
|
||||||
listWorkerOrderFeedbacks,
|
listWorkerOrderFeedbacks,
|
||||||
listWorkerSharesByWorker,
|
listWorkerSharesByWorker,
|
||||||
listWorkerWalletLedgers,
|
|
||||||
listWorkerWorkOrderNotes,
|
listWorkerWorkOrderNotes,
|
||||||
listWorkerWorkOrderViews,
|
listWorkerWorkOrderViews,
|
||||||
listWorkerWithdrawalAccounts,
|
|
||||||
listWorkCategories,
|
listWorkCategories,
|
||||||
listWorkOrders,
|
listWorkOrders,
|
||||||
listWorkOrderEventsByOrderId,
|
listWorkOrderEventsByOrderId,
|
||||||
listWorkOrderShares,
|
listWorkOrderShares,
|
||||||
listWorkOrderSharesByOrderIds,
|
listWorkOrderSharesByOrderIds,
|
||||||
listWorkerLevels,
|
|
||||||
upsertWorkerWithdrawalAccount,
|
|
||||||
getWorkerCancelRequestRisk,
|
getWorkerCancelRequestRisk,
|
||||||
listDueDepositUnfreezes,
|
|
||||||
listOverdueWorkOrders,
|
listOverdueWorkOrders,
|
||||||
releaseDepositUnfreeze,
|
|
||||||
settleOverdueWorkOrder,
|
settleOverdueWorkOrder,
|
||||||
submitWorkOrderShareAcceptance,
|
submitWorkOrderShareAcceptance,
|
||||||
updateWorkOrder,
|
updateWorkOrder,
|
||||||
@@ -65,14 +51,13 @@ import {
|
|||||||
type WorkOrderRow,
|
type WorkOrderRow,
|
||||||
type WorkOrderShareRow,
|
type WorkOrderShareRow,
|
||||||
type WorkerSessionRow,
|
type WorkerSessionRow,
|
||||||
type WorkerUserRow,
|
|
||||||
} from '../../repositories/worker-platform/index.js'
|
} from '../../repositories/worker-platform/index.js'
|
||||||
import type { JsonObject } from '../../types/json.js'
|
import type { JsonObject } from '../../types/json.js'
|
||||||
import { createHttpError } from '../../utils/http.js'
|
import { createHttpError } from '../../utils/http.js'
|
||||||
import { addHours, nowIso } from '../../utils/time.js'
|
import { addHours, nowIso } from '../../utils/time.js'
|
||||||
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.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 { getWorkerAnnouncementConfig } from './worker-announcement-config-service.js'
|
||||||
|
import { getWorkerFinanceConfig } from './worker-finance-config-service.js'
|
||||||
import { getWorkerHallConfig } from './worker-hall-config-service.js'
|
import { getWorkerHallConfig } from './worker-hall-config-service.js'
|
||||||
import {
|
import {
|
||||||
getRequiredWorkOrder,
|
getRequiredWorkOrder,
|
||||||
@@ -80,29 +65,20 @@ import {
|
|||||||
requireActiveWorkerSession,
|
requireActiveWorkerSession,
|
||||||
type WorkerSession,
|
type WorkerSession,
|
||||||
} from './worker-session-context-service.js'
|
} from './worker-session-context-service.js'
|
||||||
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
|
|
||||||
import {
|
import {
|
||||||
createWorkerAcceptanceAdminNotification,
|
createWorkerAcceptanceAdminNotification,
|
||||||
createWorkerCancelRequestAdminNotification,
|
createWorkerCancelRequestAdminNotification,
|
||||||
createWorkerOrderFeedbackAdminNotification,
|
createWorkerOrderFeedbackAdminNotification,
|
||||||
createWorkerRechargeAdminNotification,
|
|
||||||
createWorkerWithdrawAdminNotification,
|
|
||||||
getWorkerAcceptanceReminder,
|
getWorkerAcceptanceReminder,
|
||||||
remindWorkerAcceptanceAdminNotification,
|
remindWorkerAcceptanceAdminNotification,
|
||||||
} from '../admin/admin-notification-service.js'
|
} from '../admin/admin-notification-service.js'
|
||||||
import {
|
import {
|
||||||
publishWorkerFinanceRealtimeChange,
|
|
||||||
publishWorkerWalletRealtimeChange,
|
publishWorkerWalletRealtimeChange,
|
||||||
publishWorkOrderRealtimeChange,
|
publishWorkOrderRealtimeChange,
|
||||||
} from '../realtime/realtime-event-service.js'
|
} from '../realtime/realtime-event-service.js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createWorkerSession,
|
|
||||||
ensureWorkerAuthConfigured,
|
|
||||||
hashWorkerPassword,
|
|
||||||
mapCollectLookupOrder,
|
mapCollectLookupOrder,
|
||||||
mapFinanceRequest,
|
|
||||||
mapWalletLedger,
|
|
||||||
mapWorkCategory,
|
mapWorkCategory,
|
||||||
mapWorkOrderForWorker,
|
mapWorkOrderForWorker,
|
||||||
mapWorkOrderPublic,
|
mapWorkOrderPublic,
|
||||||
@@ -110,33 +86,18 @@ import {
|
|||||||
mapWorkOrderShare,
|
mapWorkOrderShare,
|
||||||
mapWorkerCancelRequest,
|
mapWorkerCancelRequest,
|
||||||
mapWorkerOrderFeedback,
|
mapWorkerOrderFeedback,
|
||||||
mapWorkerUser,
|
|
||||||
normalizeAmountFen,
|
|
||||||
assertWithdrawChannelAllowedForAmount,
|
|
||||||
normalizeBoolean,
|
normalizeBoolean,
|
||||||
normalizeFinanceRequestStatus,
|
|
||||||
normalizeFinanceRequestType,
|
|
||||||
normalizeInteger,
|
|
||||||
normalizeOptionalId,
|
normalizeOptionalId,
|
||||||
normalizePassword,
|
|
||||||
normalizePositiveInteger,
|
normalizePositiveInteger,
|
||||||
normalizeProofFiles,
|
|
||||||
normalizeSessionVersion,
|
|
||||||
normalizeStatuses,
|
normalizeStatuses,
|
||||||
normalizeStringArray,
|
normalizeStringArray,
|
||||||
normalizeSubmittedFields,
|
normalizeSubmittedFields,
|
||||||
normalizeUploadedFiles,
|
normalizeUploadedFiles,
|
||||||
normalizeUsername,
|
|
||||||
normalizeWalletLedgerType,
|
|
||||||
normalizeWithdrawChannel,
|
|
||||||
resolveFreezeDepositAmount,
|
resolveFreezeDepositAmount,
|
||||||
resolveRequirementFields,
|
resolveRequirementFields,
|
||||||
resolveWorkerPermissions,
|
resolveWorkerPermissions,
|
||||||
canWorkerAutoAcceptWholeOrder,
|
canWorkerAutoAcceptWholeOrder,
|
||||||
throwGrabWorkOrderFailure,
|
throwGrabWorkOrderFailure,
|
||||||
validateWorkerPassword,
|
|
||||||
isWorkerPasswordStrong,
|
|
||||||
verifyWorkerPassword,
|
|
||||||
} from './mappers.js'
|
} from './mappers.js'
|
||||||
import {
|
import {
|
||||||
assertWorkerLoginAllowed,
|
assertWorkerLoginAllowed,
|
||||||
@@ -153,8 +114,6 @@ import {
|
|||||||
} from './worker-login-service.js'
|
} from './worker-login-service.js'
|
||||||
import { registerWorker, resetWorkerPassword } from './worker-registration-service.js'
|
import { registerWorker, resetWorkerPassword } from './worker-registration-service.js'
|
||||||
import {
|
import {
|
||||||
hashSmsCode,
|
|
||||||
normalizeWorkerPhone,
|
|
||||||
sendWorkerLoginSmsCode,
|
sendWorkerLoginSmsCode,
|
||||||
sendWorkerPasswordResetSmsCode,
|
sendWorkerPasswordResetSmsCode,
|
||||||
sendWorkerSmsCode,
|
sendWorkerSmsCode,
|
||||||
@@ -169,9 +128,7 @@ import {
|
|||||||
} from './worker-session-auth-service.js'
|
} from './worker-session-auth-service.js'
|
||||||
import { normalizeWorkOrderTimeoutPolicy } from './work-order-timeout-policy.js'
|
import { normalizeWorkOrderTimeoutPolicy } from './work-order-timeout-policy.js'
|
||||||
|
|
||||||
const WORKER_DAILY_WITHDRAW_LIMIT = 3
|
|
||||||
const VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS = 24
|
const VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS = 24
|
||||||
const WORKER_SMS_MAX_VERIFY_ATTEMPTS = 5
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getRequiredWorkOrder,
|
getRequiredWorkOrder,
|
||||||
@@ -208,463 +165,15 @@ export {
|
|||||||
verifyWorkerSessionToken,
|
verifyWorkerSessionToken,
|
||||||
} from './worker-session-auth-service.js'
|
} from './worker-session-auth-service.js'
|
||||||
|
|
||||||
export async function getWorkerProfile(session: WorkerSession) {
|
export {
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
createWorkerRechargeRequest,
|
||||||
const [
|
createWorkerWithdrawalAccount,
|
||||||
financeSummary,
|
createWorkerWithdrawRequest,
|
||||||
acceptedOrderCount,
|
getWorkerProfile,
|
||||||
activeOrderCount,
|
listWorkerProfileFinanceRequests,
|
||||||
timeoutOrderCount,
|
listWorkerProfileWalletLedgers,
|
||||||
financeConfig,
|
saveWorkerAlipayWithdrawalQrCode,
|
||||||
withdrawalAccounts,
|
} from './worker-profile-service.js'
|
||||||
] = 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 async function listWorkerHallOrders(query: JsonObject = {}, session: WorkerSession) {
|
export async function listWorkerHallOrders(query: JsonObject = {}, session: WorkerSession) {
|
||||||
requireActiveWorkerSession(session)
|
requireActiveWorkerSession(session)
|
||||||
@@ -858,39 +367,7 @@ function resolveWorkOrderDeadlineAt(workOrder: WorkOrderRow, now: string): strin
|
|||||||
return new Date(new Date(now).getTime() + timeoutMinutes * 60 * 1000).toISOString()
|
return new Date(new Date(now).getTime() + timeoutMinutes * 60 * 1000).toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function settleDueDepositUnfreezes(
|
export { settleDueDepositUnfreezes } from './worker-profile-service.js'
|
||||||
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 async function settleOverdueWorkOrders(options: { limit?: number; workerId?: number } = {}) {
|
export async function settleOverdueWorkOrders(options: { limit?: number; workerId?: number } = {}) {
|
||||||
const overdue = await listOverdueWorkOrders({
|
const overdue = await listOverdueWorkOrders({
|
||||||
|
|||||||
Reference in New Issue
Block a user