diff --git a/apps/backend/src/services/worker-platform/worker-finance-request-service.ts b/apps/backend/src/services/worker-platform/worker-finance-request-service.ts new file mode 100644 index 00000000..f6701d15 --- /dev/null +++ b/apps/backend/src/services/worker-platform/worker-finance-request-service.ts @@ -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(), + } +} diff --git a/apps/backend/src/services/worker-platform/worker-profile-service.ts b/apps/backend/src/services/worker-platform/worker-profile-service.ts index c88f71b8..9ed16d06 100644 --- a/apps/backend/src/services/worker-platform/worker-profile-service.ts +++ b/apps/backend/src/services/worker-platform/worker-profile-service.ts @@ -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 - wechat_qr_code_json: string | Record - created_at: string -}) { - const alipayQrCodeImage = resolveAlipayWithdrawalQrCode(account) - const wechatQrCodeImage = resolveWechatWithdrawalQrCode(account) - return { - accountChannel: account.account_channel, - accountName: account.account_name, - accountNoMasked: - account.account_channel === 'alipay' ? maskWithdrawalAccountNo(account.account_no) : '', - alipayQrCodeImage, - wechatQrCodeImage, - createdAt: account.created_at, - } -} - -function resolveAlipayWithdrawalQrCode(account: { - alipay_qr_code_json: string | Record -}) { - const file = refreshUploadedFileUrls(safeParseJson(account.alipay_qr_code_json)) - return String(file.url || '').trim() ? file : null -} - -function resolveWechatWithdrawalQrCode(account: { - wechat_qr_code_json: string | Record -}) { - const file = refreshUploadedFileUrls(safeParseJson(account.wechat_qr_code_json)) - return String(file.url || '').trim() ? file : null -} - -function maskWithdrawalAccountNo(accountNo: string) { - const normalized = String(accountNo || '').trim() - if (normalized.length <= 4) return normalized - return `${'*'.repeat(Math.max(4, normalized.length - 4))}${normalized.slice(-4)}` -} - -function resolveChinaDayRange(): { start: string; end: string } { - const utcNow = new Date() - const utcDayStartMs = - Date.UTC(utcNow.getUTCFullYear(), utcNow.getUTCMonth(), utcNow.getUTCDate()) - 8 * 3_600_000 - return { - start: new Date(utcDayStartMs).toISOString(), - end: new Date(utcDayStartMs + 86_400_000).toISOString(), - } -} diff --git a/apps/backend/src/services/worker-platform/worker-withdrawal-account-service.ts b/apps/backend/src/services/worker-platform/worker-withdrawal-account-service.ts new file mode 100644 index 00000000..a1a1d8e4 --- /dev/null +++ b/apps/backend/src/services/worker-platform/worker-withdrawal-account-service.ts @@ -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 + wechat_qr_code_json: string | Record + created_at: string +}) { + const alipayQrCodeImage = resolveAlipayWithdrawalQrCode(account) + const wechatQrCodeImage = resolveWechatWithdrawalQrCode(account) + return { + accountChannel: account.account_channel, + accountName: account.account_name, + accountNoMasked: + account.account_channel === 'alipay' ? maskWithdrawalAccountNo(account.account_no) : '', + alipayQrCodeImage, + wechatQrCodeImage, + createdAt: account.created_at, + } +} + +export function resolveAlipayWithdrawalQrCode(account: { + alipay_qr_code_json: string | Record +}) { + const file = refreshUploadedFileUrls(safeParseJson(account.alipay_qr_code_json)) + return String(file.url || '').trim() ? file : null +} + +export function resolveWechatWithdrawalQrCode(account: { + wechat_qr_code_json: string | Record +}) { + const file = refreshUploadedFileUrls(safeParseJson(account.wechat_qr_code_json)) + return String(file.url || '').trim() ? file : null +} + +function maskWithdrawalAccountNo(accountNo: string) { + const normalized = String(accountNo || '').trim() + if (normalized.length <= 4) return normalized + return `${'*'.repeat(Math.max(4, normalized.length - 4))}${normalized.slice(-4)}` +}