完善支付宝提现收款码
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
-- 支付宝提现账户保存收款二维码;历史账户保留空值,需补充后才能继续提现。
|
||||
ALTER TABLE worker_withdrawal_accounts
|
||||
ADD COLUMN IF NOT EXISTS alipay_qr_code_json JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
||||
COMMENT ON COLUMN worker_withdrawal_accounts.alipay_qr_code_json IS '支付宝收款二维码文件信息';
|
||||
@@ -161,6 +161,7 @@ export type WorkerWithdrawalAccountRow = {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -654,14 +654,16 @@ export async function createWorkerWithdrawalAccount(input: {
|
||||
accountChannel: string
|
||||
accountName: string
|
||||
accountNo: string
|
||||
alipayQrCodeJson: string
|
||||
wechatQrCodeJson: string
|
||||
now: string
|
||||
}): Promise<WorkerWithdrawalAccountRow | null> {
|
||||
const result = await query<WorkerWithdrawalAccountRow>(
|
||||
`
|
||||
INSERT INTO worker_withdrawal_accounts (
|
||||
worker_id, account_channel, account_name, account_no, wechat_qr_code_json, created_at
|
||||
) VALUES ($1, $2, $3, $4, $5::jsonb, $6)
|
||||
worker_id, account_channel, account_name, account_no,
|
||||
alipay_qr_code_json, wechat_qr_code_json, created_at
|
||||
) VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7)
|
||||
ON CONFLICT (worker_id, account_channel) DO NOTHING
|
||||
RETURNING *
|
||||
`,
|
||||
@@ -670,6 +672,7 @@ export async function createWorkerWithdrawalAccount(input: {
|
||||
input.accountChannel,
|
||||
input.accountName,
|
||||
input.accountNo,
|
||||
input.alipayQrCodeJson,
|
||||
input.wechatQrCodeJson,
|
||||
input.now,
|
||||
],
|
||||
@@ -682,18 +685,21 @@ export async function upsertWorkerWithdrawalAccount(input: {
|
||||
accountChannel: string
|
||||
accountName: string
|
||||
accountNo: string
|
||||
alipayQrCodeJson: string
|
||||
wechatQrCodeJson: string
|
||||
now: string
|
||||
}): Promise<WorkerWithdrawalAccountRow | null> {
|
||||
const result = await query<WorkerWithdrawalAccountRow>(
|
||||
`
|
||||
INSERT INTO worker_withdrawal_accounts (
|
||||
worker_id, account_channel, account_name, account_no, wechat_qr_code_json, created_at
|
||||
) VALUES ($1, $2, $3, $4, $5::jsonb, $6)
|
||||
worker_id, account_channel, account_name, account_no,
|
||||
alipay_qr_code_json, wechat_qr_code_json, created_at
|
||||
) VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7)
|
||||
ON CONFLICT (worker_id, account_channel) DO UPDATE
|
||||
SET
|
||||
account_name = EXCLUDED.account_name,
|
||||
account_no = EXCLUDED.account_no,
|
||||
alipay_qr_code_json = EXCLUDED.alipay_qr_code_json,
|
||||
wechat_qr_code_json = EXCLUDED.wechat_qr_code_json
|
||||
RETURNING *
|
||||
`,
|
||||
@@ -702,6 +708,7 @@ export async function upsertWorkerWithdrawalAccount(input: {
|
||||
input.accountChannel,
|
||||
input.accountName,
|
||||
input.accountNo,
|
||||
input.alipayQrCodeJson,
|
||||
input.wechatQrCodeJson,
|
||||
input.now,
|
||||
],
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
changeWorkerPassword,
|
||||
createWorkerRechargeRequest,
|
||||
createWorkerWithdrawalAccount,
|
||||
saveWorkerAlipayWithdrawalQrCode,
|
||||
createWorkerWithdrawRequest,
|
||||
getWorkerProfile,
|
||||
getWorkerSessionSummary,
|
||||
@@ -326,6 +327,19 @@ router.post(
|
||||
),
|
||||
)
|
||||
|
||||
router.put(
|
||||
'/profile/withdrawal-account/alipay-qr-code',
|
||||
requireActiveWorker,
|
||||
createRouteHandler(
|
||||
(req) => saveWorkerAlipayWithdrawalQrCode(req.body || {}, getRequiredWorkerSession(req)),
|
||||
{
|
||||
successMessage: '支付宝收款二维码已保存',
|
||||
errorMessage: '保存支付宝收款二维码失败',
|
||||
scope: '[worker/profile/withdrawal-account/alipay-qr-code]',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/profile/withdraw-requests',
|
||||
requireActiveWorker,
|
||||
|
||||
@@ -1256,7 +1256,16 @@ export async function saveAdminWorkerWithdrawalAccount(
|
||||
const current = currentAccounts.find((item) => item.account_channel === accountChannel)
|
||||
const accountName = String(payload.accountName || payload.realName || '').trim()
|
||||
const accountNo =
|
||||
accountChannel === 'alipay' ? String(payload.accountNo || payload.account || '').trim() : ''
|
||||
accountChannel === 'alipay'
|
||||
? String(payload.accountNo || payload.account || current?.account_no || '').trim()
|
||||
: ''
|
||||
const uploadedAlipayQrCode = normalizeProofFiles(
|
||||
payload.alipayQrCodeImages || payload.alipayQrCodes,
|
||||
)[0]
|
||||
const alipayQrCodeImage =
|
||||
accountChannel === 'alipay'
|
||||
? uploadedAlipayQrCode || safeParseJson(current?.alipay_qr_code_json)
|
||||
: {}
|
||||
const uploadedWechatQrCode = normalizeProofFiles(
|
||||
payload.wechatQrCodeImages || payload.wechatQrCodes,
|
||||
)[0]
|
||||
@@ -1271,12 +1280,18 @@ export async function saveAdminWorkerWithdrawalAccount(
|
||||
errorCode: 'admin_worker_withdraw_account_name_required',
|
||||
})
|
||||
}
|
||||
if (accountChannel === 'alipay' && !accountNo) {
|
||||
if (accountChannel === 'alipay' && !current && !accountNo) {
|
||||
throw createHttpError('请填写支付宝账号', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_worker_withdraw_alipay_account_required',
|
||||
})
|
||||
}
|
||||
if (accountChannel === 'alipay' && !String(alipayQrCodeImage.url || '').trim()) {
|
||||
throw createHttpError('请上传支付宝收款二维码', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_worker_withdraw_alipay_qr_required',
|
||||
})
|
||||
}
|
||||
if (accountChannel === 'wechat' && !String(wechatQrCodeImage.url || '').trim()) {
|
||||
throw createHttpError('请上传微信收款二维码', {
|
||||
statusCode: 400,
|
||||
@@ -1289,6 +1304,7 @@ export async function saveAdminWorkerWithdrawalAccount(
|
||||
accountChannel,
|
||||
accountName,
|
||||
accountNo,
|
||||
alipayQrCodeJson: JSON.stringify(alipayQrCodeImage),
|
||||
wechatQrCodeJson: JSON.stringify(wechatQrCodeImage),
|
||||
now: nowIso(),
|
||||
})
|
||||
@@ -1414,11 +1430,13 @@ export async function reviewAdminWorkerFinanceRequest(
|
||||
}
|
||||
|
||||
function mapAdminWorkerWithdrawalAccount(account: WorkerWithdrawalAccountRow) {
|
||||
const alipayQrCodeImage = refreshUploadedFileUrls(safeParseJson(account.alipay_qr_code_json))
|
||||
const wechatQrCodeImage = refreshUploadedFileUrls(safeParseJson(account.wechat_qr_code_json))
|
||||
return {
|
||||
accountChannel: account.account_channel,
|
||||
accountName: account.account_name,
|
||||
accountNo: account.account_channel === 'alipay' ? account.account_no : '',
|
||||
alipayQrCodeImage: String(alipayQrCodeImage.url || '').trim() ? alipayQrCodeImage : null,
|
||||
wechatQrCodeImage: String(wechatQrCodeImage.url || '').trim() ? wechatQrCodeImage : null,
|
||||
createdAt: account.created_at,
|
||||
}
|
||||
|
||||
@@ -118,7 +118,10 @@ export function canWorkerAutoAcceptWholeOrder(input: {
|
||||
worker: WorkerUserRow
|
||||
hasPersonalSharingShare: boolean
|
||||
}): boolean {
|
||||
return !input.hasPersonalSharingShare && resolveWorkerPermissions(input.worker).autoAcceptWithoutEvidence
|
||||
return (
|
||||
!input.hasPersonalSharingShare &&
|
||||
resolveWorkerPermissions(input.worker).autoAcceptWithoutEvidence
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveVisibleDelaySeconds(levelKey: string, configured?: unknown): number {
|
||||
@@ -361,6 +364,10 @@ export function mapWalletLedger(ledger: WorkerWalletLedgerRow | null | undefined
|
||||
export function mapFinanceRequest(request: WorkerFinanceRequestRow | null | undefined) {
|
||||
if (!request) return null
|
||||
const payload = safeParseJson(request.payload_json)
|
||||
const alipayQrCodeImage = refreshUploadedFileUrls(payload.alipayQrCodeImage)
|
||||
if (String(alipayQrCodeImage.url || '').trim()) {
|
||||
payload.alipayQrCodeImage = alipayQrCodeImage
|
||||
}
|
||||
const wechatQrCodeImage = refreshUploadedFileUrls(payload.wechatQrCodeImage)
|
||||
if (String(wechatQrCodeImage.url || '').trim()) {
|
||||
payload.wechatQrCodeImage = wechatQrCodeImage
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
WorkOrderShareRow,
|
||||
WorkProductRuleMappingRow,
|
||||
WorkProductRuleRow,
|
||||
WorkerFinanceRequestRow,
|
||||
WorkerUserRow,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import {
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
mapWorkOrderForWorker,
|
||||
mapWorkOrderAdmin,
|
||||
mapWorkOrderSharingWorkers,
|
||||
mapFinanceRequest,
|
||||
maskWorkerLeaderboardDisplayName,
|
||||
resolveCollectSubmitTargetWorkOrder,
|
||||
resolveAdminMaterialRewardAmount,
|
||||
@@ -505,6 +507,33 @@ test('微信提现金额超过 100 元时被拒绝', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('支付宝提现申请保留收款二维码快照', () => {
|
||||
const request: WorkerFinanceRequestRow = {
|
||||
id: 1,
|
||||
worker_id: 1,
|
||||
request_type: 'withdraw',
|
||||
status: 'pending',
|
||||
amount: 10_000,
|
||||
account_channel: 'alipay',
|
||||
account_name: '测试用户',
|
||||
account_no: 'test@example.com',
|
||||
note: '',
|
||||
reviewed_note: '',
|
||||
payload_json: {
|
||||
alipayQrCodeImage: { url: 'https://example.com/alipay-qr.png' },
|
||||
},
|
||||
created_at: '2026-08-20T00:00:00.000Z',
|
||||
updated_at: '2026-08-20T00:00:00.000Z',
|
||||
reviewed_at: null,
|
||||
}
|
||||
|
||||
const mapped = mapFinanceRequest(request)
|
||||
assert.equal(
|
||||
(mapped?.payload.alipayQrCodeImage as { url?: string }).url,
|
||||
'https://example.com/alipay-qr.png',
|
||||
)
|
||||
})
|
||||
|
||||
test('提现渠道仅支持支付宝和微信', () => {
|
||||
assert.equal(normalizeWithdrawChannel('alipay'), 'alipay')
|
||||
assert.equal(normalizeWithdrawChannel('wechat'), 'wechat')
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
listWorkOrderShares,
|
||||
listWorkOrderSharesByOrderIds,
|
||||
listWorkerLevels,
|
||||
upsertWorkerWithdrawalAccount,
|
||||
getWorkerCancelRequestRisk,
|
||||
listDueDepositUnfreezes,
|
||||
listOverdueWorkOrders,
|
||||
@@ -1186,7 +1187,14 @@ export async function createWorkerWithdrawRequest(
|
||||
})
|
||||
}
|
||||
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,
|
||||
@@ -1217,6 +1225,7 @@ export async function createWorkerWithdrawRequest(
|
||||
source: 'worker_profile_withdraw',
|
||||
username: worker.username,
|
||||
phone: worker.phone || '',
|
||||
alipayQrCodeImage,
|
||||
wechatQrCodeImage,
|
||||
}),
|
||||
now: nowIso(),
|
||||
@@ -1261,6 +1270,10 @@ export async function createWorkerWithdrawalAccount(
|
||||
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
|
||||
@@ -1277,6 +1290,12 @@ export async function createWorkerWithdrawalAccount(
|
||||
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,
|
||||
@@ -1289,6 +1308,7 @@ export async function createWorkerWithdrawalAccount(
|
||||
accountChannel,
|
||||
accountName,
|
||||
accountNo,
|
||||
alipayQrCodeJson: JSON.stringify(alipayQrCodeImage || {}),
|
||||
wechatQrCodeJson: JSON.stringify(wechatQrCodeImage || {}),
|
||||
now: nowIso(),
|
||||
})
|
||||
@@ -1303,24 +1323,81 @@ export async function createWorkerWithdrawalAccount(
|
||||
}
|
||||
}
|
||||
|
||||
/** 历史支付宝账户只允许补充一次收款二维码,不允许修改账户姓名、账号或二维码。 */
|
||||
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>
|
||||
}) {
|
||||
|
||||
Reference in New Issue
Block a user