拆分打手个人中心读取与资金服务
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
import {
|
||||
countWorkerWithdrawRequestsOnDay,
|
||||
createWorkerFinanceRequest,
|
||||
getWorkerFinanceRequestSummary,
|
||||
getWorkerWithdrawalAccount,
|
||||
listDueDepositUnfreezes,
|
||||
releaseDepositUnfreeze,
|
||||
} 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 {
|
||||
createWorkerRechargeAdminNotification,
|
||||
createWorkerWithdrawAdminNotification,
|
||||
} from '../admin/admin-notification-service.js'
|
||||
import {
|
||||
publishWorkerFinanceRealtimeChange,
|
||||
publishWorkerWalletRealtimeChange,
|
||||
} from '../realtime/realtime-event-service.js'
|
||||
import {
|
||||
assertWithdrawChannelAllowedForAmount,
|
||||
mapFinanceRequest,
|
||||
normalizeAmountFen,
|
||||
normalizeProofFiles,
|
||||
normalizeWithdrawChannel,
|
||||
} from './mappers.js'
|
||||
import { getWorkerFinanceConfig } from './worker-finance-config-service.js'
|
||||
import {
|
||||
resolveAlipayWithdrawalQrCode,
|
||||
resolveWechatWithdrawalQrCode,
|
||||
} from './worker-withdrawal-account-service.js'
|
||||
import { getRequiredWorker, requireActiveWorkerSession, type WorkerSession } from './worker-session-context-service.js'
|
||||
|
||||
const WORKER_DAILY_WITHDRAW_LIMIT = 3
|
||||
|
||||
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 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(),
|
||||
}
|
||||
}
|
||||
@@ -3,55 +3,45 @@ 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'
|
||||
normalizePage,
|
||||
normalizePageSize,
|
||||
safeParseJson,
|
||||
} from '../admin/admin-query-utils.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 {
|
||||
createWorkerRechargeRequest,
|
||||
createWorkerWithdrawRequest,
|
||||
settleDueDepositUnfreezes,
|
||||
} from './worker-finance-request-service.js'
|
||||
export {
|
||||
createWorkerWithdrawalAccount,
|
||||
saveWorkerAlipayWithdrawalQrCode,
|
||||
} from './worker-withdrawal-account-service.js'
|
||||
import { mapWithdrawalAccount } from './worker-withdrawal-account-service.js'
|
||||
|
||||
export async function getWorkerProfile(session: WorkerSession) {
|
||||
const worker = await getRequiredWorker(session.workerId)
|
||||
@@ -175,373 +165,3 @@ export async function listWorkerProfileFinanceRequests(
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
createWorkerWithdrawalAccount as createWorkerWithdrawalAccountRecord,
|
||||
getWorkerWithdrawalAccount,
|
||||
upsertWorkerWithdrawalAccount,
|
||||
} 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 { safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
|
||||
import { normalizeProofFiles, normalizeWithdrawChannel } from './mappers.js'
|
||||
import { getRequiredWorker, requireActiveWorkerSession, type WorkerSession } from './worker-session-context-service.js'
|
||||
|
||||
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 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,
|
||||
}
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
||||
|
||||
export 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)}`
|
||||
}
|
||||
Reference in New Issue
Block a user