完善支付宝提现收款码

This commit is contained in:
yml2213
2026-08-21 00:00:05 +08:00
parent 81a412323b
commit 361b84931f
14 changed files with 362 additions and 35 deletions
@@ -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_channel: string
account_name: string account_name: string
account_no: string account_no: string
alipay_qr_code_json: string | Record<string, unknown>
wechat_qr_code_json: string | Record<string, unknown> wechat_qr_code_json: string | Record<string, unknown>
created_at: string created_at: string
} }
@@ -654,14 +654,16 @@ export async function createWorkerWithdrawalAccount(input: {
accountChannel: string accountChannel: string
accountName: string accountName: string
accountNo: string accountNo: string
alipayQrCodeJson: string
wechatQrCodeJson: string wechatQrCodeJson: string
now: string now: string
}): Promise<WorkerWithdrawalAccountRow | null> { }): Promise<WorkerWithdrawalAccountRow | null> {
const result = await query<WorkerWithdrawalAccountRow>( const result = await query<WorkerWithdrawalAccountRow>(
` `
INSERT INTO worker_withdrawal_accounts ( INSERT INTO worker_withdrawal_accounts (
worker_id, account_channel, account_name, account_no, wechat_qr_code_json, created_at worker_id, account_channel, account_name, account_no,
) VALUES ($1, $2, $3, $4, $5::jsonb, $6) 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 ON CONFLICT (worker_id, account_channel) DO NOTHING
RETURNING * RETURNING *
`, `,
@@ -670,6 +672,7 @@ export async function createWorkerWithdrawalAccount(input: {
input.accountChannel, input.accountChannel,
input.accountName, input.accountName,
input.accountNo, input.accountNo,
input.alipayQrCodeJson,
input.wechatQrCodeJson, input.wechatQrCodeJson,
input.now, input.now,
], ],
@@ -682,18 +685,21 @@ export async function upsertWorkerWithdrawalAccount(input: {
accountChannel: string accountChannel: string
accountName: string accountName: string
accountNo: string accountNo: string
alipayQrCodeJson: string
wechatQrCodeJson: string wechatQrCodeJson: string
now: string now: string
}): Promise<WorkerWithdrawalAccountRow | null> { }): Promise<WorkerWithdrawalAccountRow | null> {
const result = await query<WorkerWithdrawalAccountRow>( const result = await query<WorkerWithdrawalAccountRow>(
` `
INSERT INTO worker_withdrawal_accounts ( INSERT INTO worker_withdrawal_accounts (
worker_id, account_channel, account_name, account_no, wechat_qr_code_json, created_at worker_id, account_channel, account_name, account_no,
) VALUES ($1, $2, $3, $4, $5::jsonb, $6) 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 ON CONFLICT (worker_id, account_channel) DO UPDATE
SET SET
account_name = EXCLUDED.account_name, account_name = EXCLUDED.account_name,
account_no = EXCLUDED.account_no, account_no = EXCLUDED.account_no,
alipay_qr_code_json = EXCLUDED.alipay_qr_code_json,
wechat_qr_code_json = EXCLUDED.wechat_qr_code_json wechat_qr_code_json = EXCLUDED.wechat_qr_code_json
RETURNING * RETURNING *
`, `,
@@ -702,6 +708,7 @@ export async function upsertWorkerWithdrawalAccount(input: {
input.accountChannel, input.accountChannel,
input.accountName, input.accountName,
input.accountNo, input.accountNo,
input.alipayQrCodeJson,
input.wechatQrCodeJson, input.wechatQrCodeJson,
input.now, input.now,
], ],
+14
View File
@@ -7,6 +7,7 @@ import {
changeWorkerPassword, changeWorkerPassword,
createWorkerRechargeRequest, createWorkerRechargeRequest,
createWorkerWithdrawalAccount, createWorkerWithdrawalAccount,
saveWorkerAlipayWithdrawalQrCode,
createWorkerWithdrawRequest, createWorkerWithdrawRequest,
getWorkerProfile, getWorkerProfile,
getWorkerSessionSummary, 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( router.post(
'/profile/withdraw-requests', '/profile/withdraw-requests',
requireActiveWorker, requireActiveWorker,
@@ -1256,7 +1256,16 @@ export async function saveAdminWorkerWithdrawalAccount(
const current = currentAccounts.find((item) => item.account_channel === accountChannel) const current = currentAccounts.find((item) => item.account_channel === accountChannel)
const accountName = String(payload.accountName || payload.realName || '').trim() const accountName = String(payload.accountName || payload.realName || '').trim()
const accountNo = 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( const uploadedWechatQrCode = normalizeProofFiles(
payload.wechatQrCodeImages || payload.wechatQrCodes, payload.wechatQrCodeImages || payload.wechatQrCodes,
)[0] )[0]
@@ -1271,12 +1280,18 @@ export async function saveAdminWorkerWithdrawalAccount(
errorCode: 'admin_worker_withdraw_account_name_required', errorCode: 'admin_worker_withdraw_account_name_required',
}) })
} }
if (accountChannel === 'alipay' && !accountNo) { if (accountChannel === 'alipay' && !current && !accountNo) {
throw createHttpError('请填写支付宝账号', { throw createHttpError('请填写支付宝账号', {
statusCode: 400, statusCode: 400,
errorCode: 'admin_worker_withdraw_alipay_account_required', 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()) { if (accountChannel === 'wechat' && !String(wechatQrCodeImage.url || '').trim()) {
throw createHttpError('请上传微信收款二维码', { throw createHttpError('请上传微信收款二维码', {
statusCode: 400, statusCode: 400,
@@ -1289,6 +1304,7 @@ export async function saveAdminWorkerWithdrawalAccount(
accountChannel, accountChannel,
accountName, accountName,
accountNo, accountNo,
alipayQrCodeJson: JSON.stringify(alipayQrCodeImage),
wechatQrCodeJson: JSON.stringify(wechatQrCodeImage), wechatQrCodeJson: JSON.stringify(wechatQrCodeImage),
now: nowIso(), now: nowIso(),
}) })
@@ -1414,11 +1430,13 @@ export async function reviewAdminWorkerFinanceRequest(
} }
function mapAdminWorkerWithdrawalAccount(account: WorkerWithdrawalAccountRow) { function mapAdminWorkerWithdrawalAccount(account: WorkerWithdrawalAccountRow) {
const alipayQrCodeImage = refreshUploadedFileUrls(safeParseJson(account.alipay_qr_code_json))
const wechatQrCodeImage = refreshUploadedFileUrls(safeParseJson(account.wechat_qr_code_json)) const wechatQrCodeImage = refreshUploadedFileUrls(safeParseJson(account.wechat_qr_code_json))
return { return {
accountChannel: account.account_channel, accountChannel: account.account_channel,
accountName: account.account_name, accountName: account.account_name,
accountNo: account.account_channel === 'alipay' ? account.account_no : '', accountNo: account.account_channel === 'alipay' ? account.account_no : '',
alipayQrCodeImage: String(alipayQrCodeImage.url || '').trim() ? alipayQrCodeImage : null,
wechatQrCodeImage: String(wechatQrCodeImage.url || '').trim() ? wechatQrCodeImage : null, wechatQrCodeImage: String(wechatQrCodeImage.url || '').trim() ? wechatQrCodeImage : null,
createdAt: account.created_at, createdAt: account.created_at,
} }
@@ -118,7 +118,10 @@ export function canWorkerAutoAcceptWholeOrder(input: {
worker: WorkerUserRow worker: WorkerUserRow
hasPersonalSharingShare: boolean hasPersonalSharingShare: boolean
}): boolean { }): boolean {
return !input.hasPersonalSharingShare && resolveWorkerPermissions(input.worker).autoAcceptWithoutEvidence return (
!input.hasPersonalSharingShare &&
resolveWorkerPermissions(input.worker).autoAcceptWithoutEvidence
)
} }
export function resolveVisibleDelaySeconds(levelKey: string, configured?: unknown): number { 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) { export function mapFinanceRequest(request: WorkerFinanceRequestRow | null | undefined) {
if (!request) return null if (!request) return null
const payload = safeParseJson(request.payload_json) const payload = safeParseJson(request.payload_json)
const alipayQrCodeImage = refreshUploadedFileUrls(payload.alipayQrCodeImage)
if (String(alipayQrCodeImage.url || '').trim()) {
payload.alipayQrCodeImage = alipayQrCodeImage
}
const wechatQrCodeImage = refreshUploadedFileUrls(payload.wechatQrCodeImage) const wechatQrCodeImage = refreshUploadedFileUrls(payload.wechatQrCodeImage)
if (String(wechatQrCodeImage.url || '').trim()) { if (String(wechatQrCodeImage.url || '').trim()) {
payload.wechatQrCodeImage = wechatQrCodeImage payload.wechatQrCodeImage = wechatQrCodeImage
@@ -6,6 +6,7 @@ import type {
WorkOrderShareRow, WorkOrderShareRow,
WorkProductRuleMappingRow, WorkProductRuleMappingRow,
WorkProductRuleRow, WorkProductRuleRow,
WorkerFinanceRequestRow,
WorkerUserRow, WorkerUserRow,
} from '../../repositories/worker-platform/index.js' } from '../../repositories/worker-platform/index.js'
import { import {
@@ -26,6 +27,7 @@ import {
mapWorkOrderForWorker, mapWorkOrderForWorker,
mapWorkOrderAdmin, mapWorkOrderAdmin,
mapWorkOrderSharingWorkers, mapWorkOrderSharingWorkers,
mapFinanceRequest,
maskWorkerLeaderboardDisplayName, maskWorkerLeaderboardDisplayName,
resolveCollectSubmitTargetWorkOrder, resolveCollectSubmitTargetWorkOrder,
resolveAdminMaterialRewardAmount, 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('提现渠道仅支持支付宝和微信', () => { test('提现渠道仅支持支付宝和微信', () => {
assert.equal(normalizeWithdrawChannel('alipay'), 'alipay') assert.equal(normalizeWithdrawChannel('alipay'), 'alipay')
assert.equal(normalizeWithdrawChannel('wechat'), 'wechat') assert.equal(normalizeWithdrawChannel('wechat'), 'wechat')
@@ -57,6 +57,7 @@ import {
listWorkOrderShares, listWorkOrderShares,
listWorkOrderSharesByOrderIds, listWorkOrderSharesByOrderIds,
listWorkerLevels, listWorkerLevels,
upsertWorkerWithdrawalAccount,
getWorkerCancelRequestRisk, getWorkerCancelRequestRisk,
listDueDepositUnfreezes, listDueDepositUnfreezes,
listOverdueWorkOrders, listOverdueWorkOrders,
@@ -1186,7 +1187,14 @@ export async function createWorkerWithdrawRequest(
}) })
} }
assertWithdrawChannelAllowedForAmount(amount, accountChannel) assertWithdrawChannelAllowedForAmount(amount, accountChannel)
const alipayQrCodeImage = resolveAlipayWithdrawalQrCode(withdrawalAccount)
const wechatQrCodeImage = resolveWechatWithdrawalQrCode(withdrawalAccount) const wechatQrCodeImage = resolveWechatWithdrawalQrCode(withdrawalAccount)
if (accountChannel === 'alipay' && !alipayQrCodeImage) {
throw createHttpError('支付宝提现信息缺少收款二维码,请先补充后再提现', {
statusCode: 409,
errorCode: 'worker_withdrawal_alipay_qr_required',
})
}
if (accountChannel === 'wechat' && !wechatQrCodeImage) { if (accountChannel === 'wechat' && !wechatQrCodeImage) {
throw createHttpError('微信提现信息缺少收款二维码,请联系客服处理', { throw createHttpError('微信提现信息缺少收款二维码,请联系客服处理', {
statusCode: 409, statusCode: 409,
@@ -1217,6 +1225,7 @@ export async function createWorkerWithdrawRequest(
source: 'worker_profile_withdraw', source: 'worker_profile_withdraw',
username: worker.username, username: worker.username,
phone: worker.phone || '', phone: worker.phone || '',
alipayQrCodeImage,
wechatQrCodeImage, wechatQrCodeImage,
}), }),
now: nowIso(), now: nowIso(),
@@ -1261,6 +1270,10 @@ export async function createWorkerWithdrawalAccount(
const accountName = String(payload.accountName || payload.realName || '').trim() const accountName = String(payload.accountName || payload.realName || '').trim()
const accountNo = const accountNo =
accountChannel === 'alipay' ? String(payload.accountNo || payload.account || '').trim() : '' accountChannel === 'alipay' ? String(payload.accountNo || payload.account || '').trim() : ''
const alipayQrCodeImage =
accountChannel === 'alipay'
? normalizeProofFiles(payload.alipayQrCodeImages || payload.alipayQrCodes)[0] || null
: null
const wechatQrCodeImage = const wechatQrCodeImage =
accountChannel === 'wechat' accountChannel === 'wechat'
? normalizeProofFiles(payload.wechatQrCodeImages || payload.wechatQrCodes)[0] || null ? normalizeProofFiles(payload.wechatQrCodeImages || payload.wechatQrCodes)[0] || null
@@ -1277,6 +1290,12 @@ export async function createWorkerWithdrawalAccount(
errorCode: 'worker_withdraw_account_no_required', errorCode: 'worker_withdraw_account_no_required',
}) })
} }
if (accountChannel === 'alipay' && !alipayQrCodeImage) {
throw createHttpError('请上传支付宝收款二维码', {
statusCode: 400,
errorCode: 'worker_withdraw_alipay_qr_required',
})
}
if (accountChannel === 'wechat' && !wechatQrCodeImage) { if (accountChannel === 'wechat' && !wechatQrCodeImage) {
throw createHttpError('请上传微信收款二维码', { throw createHttpError('请上传微信收款二维码', {
statusCode: 400, statusCode: 400,
@@ -1289,6 +1308,7 @@ export async function createWorkerWithdrawalAccount(
accountChannel, accountChannel,
accountName, accountName,
accountNo, accountNo,
alipayQrCodeJson: JSON.stringify(alipayQrCodeImage || {}),
wechatQrCodeJson: JSON.stringify(wechatQrCodeImage || {}), wechatQrCodeJson: JSON.stringify(wechatQrCodeImage || {}),
now: nowIso(), 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: { function mapWithdrawalAccount(account: {
account_channel: string account_channel: string
account_name: string account_name: string
account_no: string account_no: string
alipay_qr_code_json: string | Record<string, unknown>
wechat_qr_code_json: string | Record<string, unknown> wechat_qr_code_json: string | Record<string, unknown>
created_at: string created_at: string
}) { }) {
const alipayQrCodeImage = resolveAlipayWithdrawalQrCode(account)
const wechatQrCodeImage = resolveWechatWithdrawalQrCode(account) const wechatQrCodeImage = resolveWechatWithdrawalQrCode(account)
return { return {
accountChannel: account.account_channel, accountChannel: account.account_channel,
accountName: account.account_name, accountName: account.account_name,
accountNoMasked: accountNoMasked:
account.account_channel === 'alipay' ? maskWithdrawalAccountNo(account.account_no) : '', account.account_channel === 'alipay' ? maskWithdrawalAccountNo(account.account_no) : '',
alipayQrCodeImage,
wechatQrCodeImage, wechatQrCodeImage,
createdAt: account.created_at, 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: { function resolveWechatWithdrawalQrCode(account: {
wechat_qr_code_json: string | Record<string, unknown> wechat_qr_code_json: string | Record<string, unknown>
}) { }) {
@@ -235,9 +235,7 @@ export default function FinancePanel() {
minWidth: 140, minWidth: 140,
render: (_, row) => ( render: (_, row) => (
<ImagePreviewList <ImagePreviewList
files={ files={row.requestType === 'recharge' ? getRechargeProofs(row) : getWithdrawQrCode(row)}
row.requestType === 'recharge' ? getRechargeProofs(row) : getWechatWithdrawQrCode(row)
}
size={48} size={48}
/> />
), ),
@@ -516,6 +514,7 @@ export default function FinancePanel() {
</Descriptions.Item> </Descriptions.Item>
{reviewState.request.requestType === 'withdraw' && {reviewState.request.requestType === 'withdraw' &&
reviewState.request.accountChannel === 'alipay' ? ( reviewState.request.accountChannel === 'alipay' ? (
<>
<Descriptions.Item label="支付宝账号"> <Descriptions.Item label="支付宝账号">
{reviewState.request.accountNo ? ( {reviewState.request.accountNo ? (
<Typography.Text copyable>{reviewState.request.accountNo}</Typography.Text> <Typography.Text copyable>{reviewState.request.accountNo}</Typography.Text>
@@ -523,6 +522,13 @@ export default function FinancePanel() {
'-' '-'
)} )}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="支付宝收款二维码">
<ImagePreviewList
files={getAlipayWithdrawQrCode(reviewState.request)}
size={88}
/>
</Descriptions.Item>
</>
) : null} ) : null}
{reviewState.request.requestType === 'recharge' ? ( {reviewState.request.requestType === 'recharge' ? (
<> <>
@@ -697,6 +703,30 @@ function getWechatWithdrawQrCode(request: WorkerFinanceRequest): UploadedFile[]
] ]
} }
function getWithdrawQrCode(request: WorkerFinanceRequest): UploadedFile[] {
if (request.accountChannel === 'alipay') return getAlipayWithdrawQrCode(request)
return getWechatWithdrawQrCode(request)
}
function getAlipayWithdrawQrCode(request: WorkerFinanceRequest): UploadedFile[] {
if (request.requestType !== 'withdraw' || request.accountChannel !== 'alipay') return []
const payload = asRecord(request.payload)
const file = asRecord(payload.alipayQrCodeImage)
const url = String(file.url || '').trim()
if (!url) return []
return [
{
url,
mediumUrl: String(file.mediumUrl || '').trim(),
thumbnailUrl: String(file.thumbnailUrl || '').trim(),
objectKey: String(file.objectKey || '').trim(),
filename: String(file.filename || '').trim(),
contentType: String(file.contentType || '').trim(),
size: Number(file.size || 0),
},
]
}
function getRechargePaidAt(request: WorkerFinanceRequest): string { function getRechargePaidAt(request: WorkerFinanceRequest): string {
const payload = asRecord(request.payload) const payload = asRecord(request.payload)
return String(payload.paidAt || '').trim() return String(payload.paidAt || '').trim()
@@ -39,6 +39,7 @@ type WithdrawalAccountFormValues = {
accountChannel?: string accountChannel?: string
accountName?: string accountName?: string
accountNo?: string accountNo?: string
alipayQrCodeImages?: UploadedFile[]
wechatQrCodeImages?: UploadedFile[] wechatQrCodeImages?: UploadedFile[]
} }
@@ -69,6 +70,9 @@ export default function WorkersPanel() {
const [resetPasswordForm] = Form.useForm<{ password: string }>() const [resetPasswordForm] = Form.useForm<{ password: string }>()
const [freezeForm] = Form.useForm<FreezeWorkerFormValues>() const [freezeForm] = Form.useForm<FreezeWorkerFormValues>()
const withdrawalAccountChannel = Form.useWatch('accountChannel', withdrawalAccountForm) const withdrawalAccountChannel = Form.useWatch('accountChannel', withdrawalAccountForm)
const editingAlipayAccount = withdrawalAccounts.find(
(account) => account.accountChannel === 'alipay',
)
const workersQuery = useQuery({ const workersQuery = useQuery({
queryKey: ['admin-worker-platform-workers', status, keyword, workerType, page, pageSize], queryKey: ['admin-worker-platform-workers', status, keyword, workerType, page, pageSize],
@@ -225,6 +229,7 @@ export default function WorkersPanel() {
accountChannel, accountChannel,
accountName: account?.accountName || '', accountName: account?.accountName || '',
accountNo: account?.accountNo || '', accountNo: account?.accountNo || '',
alipayQrCodeImages: account?.alipayQrCodeImage ? [account.alipayQrCodeImage] : [],
wechatQrCodeImages: account?.wechatQrCodeImage ? [account.wechatQrCodeImage] : [], wechatQrCodeImages: account?.wechatQrCodeImage ? [account.wechatQrCodeImage] : [],
}) })
} }
@@ -240,6 +245,7 @@ export default function WorkersPanel() {
{ {
accountName: String(values.accountName || '').trim(), accountName: String(values.accountName || '').trim(),
accountNo: String(values.accountNo || '').trim(), accountNo: String(values.accountNo || '').trim(),
alipayQrCodeImages: values.alipayQrCodeImages || [],
wechatQrCodeImages: values.wechatQrCodeImages || [], wechatQrCodeImages: values.wechatQrCodeImages || [],
}, },
) )
@@ -559,13 +565,28 @@ export default function WorkersPanel() {
<ImageUpload scene="admin-worker-withdrawal-wechat-qr" scope="admin" maxCount={1} /> <ImageUpload scene="admin-worker-withdrawal-wechat-qr" scope="admin" maxCount={1} />
</Form.Item> </Form.Item>
) : ( ) : (
<>
<Form.Item <Form.Item
label="支付宝账号" label="支付宝账号"
name="accountNo" name="accountNo"
rules={[{ required: true, message: '请输入支付宝账号' }]} rules={
editingAlipayAccount ? [] : [{ required: true, message: '请输入支付宝账号' }]
}
> >
<Input placeholder="请输入支付宝账号" /> <Input
placeholder={
editingAlipayAccount ? '历史账户未登记账号,可仅补充二维码' : '请输入支付宝账号'
}
/>
</Form.Item> </Form.Item>
<Form.Item
label="支付宝收款二维码"
name="alipayQrCodeImages"
rules={[{ required: true, message: '请上传支付宝收款二维码' }]}
>
<ImageUpload scene="admin-worker-withdrawal-alipay-qr" scope="admin" maxCount={1} />
</Form.Item>
</>
)} )}
</Form> </Form>
</Modal> </Modal>
@@ -49,6 +49,7 @@ import {
deleteWorkerSession, deleteWorkerSession,
fetchWorkerWalletLedgers, fetchWorkerWalletLedgers,
logoutWorker, logoutWorker,
saveWorkerAlipayWithdrawalQrCode,
} from '@/services/worker' } from '@/services/worker'
import type { import type {
UploadedFile, UploadedFile,
@@ -80,9 +81,14 @@ type WithdrawalAccountFormValues = {
accountChannel?: string accountChannel?: string
accountName?: string accountName?: string
accountNo?: string accountNo?: string
alipayQrCodeImages?: UploadedFile[]
wechatQrCodeImages?: UploadedFile[] wechatQrCodeImages?: UploadedFile[]
} }
type AlipayQrCodeFormValues = {
alipayQrCodeImages?: UploadedFile[]
}
type PasswordFormValues = { type PasswordFormValues = {
currentPassword?: string currentPassword?: string
newPassword?: string newPassword?: string
@@ -106,6 +112,7 @@ export default function WorkerProfilePage() {
const [rechargeOpen, setRechargeOpen] = useState(false) const [rechargeOpen, setRechargeOpen] = useState(false)
const [withdrawOpen, setWithdrawOpen] = useState(false) const [withdrawOpen, setWithdrawOpen] = useState(false)
const [withdrawalAccountOpen, setWithdrawalAccountOpen] = useState(false) const [withdrawalAccountOpen, setWithdrawalAccountOpen] = useState(false)
const [alipayQrCodeOpen, setAlipayQrCodeOpen] = useState(false)
const [passwordOpen, setPasswordOpen] = useState(false) const [passwordOpen, setPasswordOpen] = useState(false)
const [contactOpen, setContactOpen] = useState(false) const [contactOpen, setContactOpen] = useState(false)
const [devicesOpen, setDevicesOpen] = useState(false) const [devicesOpen, setDevicesOpen] = useState(false)
@@ -116,6 +123,7 @@ export default function WorkerProfilePage() {
const [rechargeForm] = Form.useForm<RechargeFormValues>() const [rechargeForm] = Form.useForm<RechargeFormValues>()
const [withdrawForm] = Form.useForm<WithdrawFormValues>() const [withdrawForm] = Form.useForm<WithdrawFormValues>()
const [withdrawalAccountForm] = Form.useForm<WithdrawalAccountFormValues>() const [withdrawalAccountForm] = Form.useForm<WithdrawalAccountFormValues>()
const [alipayQrCodeForm] = Form.useForm<AlipayQrCodeFormValues>()
const [passwordForm] = Form.useForm<PasswordFormValues>() const [passwordForm] = Form.useForm<PasswordFormValues>()
const selectedWithdrawChannel = Form.useWatch('accountChannel', withdrawForm) const selectedWithdrawChannel = Form.useWatch('accountChannel', withdrawForm)
const selectedWithdrawalAccountChannel = Form.useWatch('accountChannel', withdrawalAccountForm) const selectedWithdrawalAccountChannel = Form.useWatch('accountChannel', withdrawalAccountForm)
@@ -141,6 +149,9 @@ export default function WorkerProfilePage() {
const selectedWithdrawalAccount = withdrawalAccounts.find( const selectedWithdrawalAccount = withdrawalAccounts.find(
(account) => account.accountChannel === selectedWithdrawChannel, (account) => account.accountChannel === selectedWithdrawChannel,
) )
const alipayWithdrawalAccount = withdrawalAccounts.find(
(account) => account.accountChannel === 'alipay',
)
const canUseFinanceActions = worker?.status === 'active' const canUseFinanceActions = worker?.status === 'active'
async function copyInviteCode() { async function copyInviteCode() {
@@ -233,6 +244,11 @@ export default function WorkerProfilePage() {
openWithdrawalAccountModal() openWithdrawalAccountModal()
return return
} }
if (alipayWithdrawalAccount && !alipayWithdrawalAccount.alipayQrCodeImage) {
message.warning('请先补充支付宝收款二维码')
openAlipayQrCodeModal()
return
}
withdrawForm.setFieldValue('accountChannel', withdrawalAccounts[0].accountChannel) withdrawForm.setFieldValue('accountChannel', withdrawalAccounts[0].accountChannel)
setWithdrawOpen(true) setWithdrawOpen(true)
} }
@@ -249,6 +265,19 @@ export default function WorkerProfilePage() {
setWithdrawalAccountOpen(true) setWithdrawalAccountOpen(true)
} }
function openAlipayQrCodeModal() {
if (!alipayWithdrawalAccount) {
openWithdrawalAccountModal()
return
}
alipayQrCodeForm.setFieldsValue({
alipayQrCodeImages: alipayWithdrawalAccount.alipayQrCodeImage
? [alipayWithdrawalAccount.alipayQrCodeImage]
: [],
})
setAlipayQrCodeOpen(true)
}
async function submitRecharge(values: RechargeFormValues) { async function submitRecharge(values: RechargeFormValues) {
setSubmittingRecharge(true) setSubmittingRecharge(true)
try { try {
@@ -301,6 +330,7 @@ export default function WorkerProfilePage() {
accountChannel: String(values.accountChannel || ''), accountChannel: String(values.accountChannel || ''),
accountName: String(values.accountName || '').trim(), accountName: String(values.accountName || '').trim(),
accountNo: String(values.accountNo || '').trim(), accountNo: String(values.accountNo || '').trim(),
alipayQrCodeImages: values.alipayQrCodeImages || [],
wechatQrCodeImages: values.wechatQrCodeImages || [], wechatQrCodeImages: values.wechatQrCodeImages || [],
}) })
message.success('提现信息已添加,该方式后续修改请联系客服处理') message.success('提现信息已添加,该方式后续修改请联系客服处理')
@@ -316,6 +346,20 @@ export default function WorkerProfilePage() {
} }
} }
async function submitAlipayQrCode(values: AlipayQrCodeFormValues) {
try {
await saveWorkerAlipayWithdrawalQrCode({
alipayQrCodeImages: values.alipayQrCodeImages || [],
})
message.success('支付宝收款二维码已保存')
setAlipayQrCodeOpen(false)
alipayQrCodeForm.resetFields()
await refreshAll()
} catch (error) {
message.error(error instanceof Error ? error.message : '保存支付宝收款二维码失败')
}
}
async function submitPasswordChange(values: PasswordFormValues) { async function submitPasswordChange(values: PasswordFormValues) {
setSubmittingPassword(true) setSubmittingPassword(true)
try { try {
@@ -673,6 +717,22 @@ export default function WorkerProfilePage() {
</div> </div>
<span></span> <span></span>
</button> </button>
{alipayWithdrawalAccount && !alipayWithdrawalAccount.alipayQrCodeImage ? (
<button
type="button"
className="worker-profile-mobile-tool-item"
disabled={!canUseFinanceActions}
onClick={openAlipayQrCodeModal}
>
<div
className="worker-profile-mobile-tool-icon"
style={{ background: '#f5f3ff', color: '#7c3aed' }}
>
<BankOutlined />
</div>
<span></span>
</button>
) : null}
<button <button
type="button" type="button"
className="worker-profile-mobile-tool-item" className="worker-profile-mobile-tool-item"
@@ -1152,6 +1212,15 @@ export default function WorkerProfilePage() {
</Button> </Button>
) : null} ) : null}
{alipayWithdrawalAccount && !alipayWithdrawalAccount.alipayQrCodeImage ? (
<Button
icon={<BankOutlined />}
disabled={!canUseFinanceActions}
onClick={openAlipayQrCodeModal}
>
</Button>
) : null}
<Button icon={<LockOutlined />} onClick={() => setPasswordOpen(true)}> <Button icon={<LockOutlined />} onClick={() => setPasswordOpen(true)}>
</Button> </Button>
@@ -1234,10 +1303,14 @@ export default function WorkerProfilePage() {
<Typography.Text type="secondary"> <Typography.Text type="secondary">
{account.accountChannel === 'wechat' {account.accountChannel === 'wechat'
? '已上传收款二维码' ? '已上传收款二维码'
: account.accountNoMasked} : `${account.accountNoMasked} / ${
account.alipayQrCodeImage
? '已上传收款二维码'
: '未上传收款二维码'
}`}
</Typography.Text> </Typography.Text>
<Typography.Text type="secondary"> <Typography.Text type="secondary">
</Typography.Text> </Typography.Text>
</Space> </Space>
))} ))}
@@ -1533,7 +1606,7 @@ export default function WorkerProfilePage() {
<Alert <Alert
showIcon showIcon
type="warning" type="warning"
message="支付宝和微信可分别添加一次;每种方式添加后均无法自行修改,如需修改请联系客服。" message="支付宝和微信可分别添加一次;收款人姓名和账号添加后不可自行修改。历史支付宝账户缺少二维码时,仅可补充一次。"
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
/> />
<Form form={withdrawalAccountForm} layout="vertical" onFinish={submitWithdrawalAccount}> <Form form={withdrawalAccountForm} layout="vertical" onFinish={submitWithdrawalAccount}>
@@ -1570,6 +1643,7 @@ export default function WorkerProfilePage() {
<ImageUpload scene="worker-withdrawal-wechat-qr" scope="worker" maxCount={1} /> <ImageUpload scene="worker-withdrawal-wechat-qr" scope="worker" maxCount={1} />
</Form.Item> </Form.Item>
) : ( ) : (
<>
<Form.Item <Form.Item
label="支付宝账号" label="支付宝账号"
name="accountNo" name="accountNo"
@@ -1577,10 +1651,46 @@ export default function WorkerProfilePage() {
> >
<Input placeholder="请输入支付宝账号" /> <Input placeholder="请输入支付宝账号" />
</Form.Item> </Form.Item>
<Form.Item
label="支付宝收款二维码"
name="alipayQrCodeImages"
rules={[{ required: true, message: '请上传支付宝收款二维码' }]}
>
<ImageUpload scene="worker-withdrawal-alipay-qr" scope="worker" maxCount={1} />
</Form.Item>
</>
)} )}
</Form> </Form>
</Modal> </Modal>
<Modal
title="补充支付宝收款二维码"
open={alipayQrCodeOpen}
width={isMobile ? '92%' : 500}
destroyOnHidden
onCancel={() => {
setAlipayQrCodeOpen(false)
alipayQrCodeForm.resetFields()
}}
onOk={() => alipayQrCodeForm.submit()}
>
<Alert
showIcon
type="info"
message="历史账户仅可补充一次支付宝收款二维码,保存后不可自行修改;已登记的收款人姓名和账号保持锁定。"
style={{ marginBottom: 16 }}
/>
<Form form={alipayQrCodeForm} layout="vertical" onFinish={submitAlipayQrCode}>
<Form.Item
label="支付宝收款二维码"
name="alipayQrCodeImages"
rules={[{ required: true, message: '请上传支付宝收款二维码' }]}
>
<ImageUpload scene="worker-withdrawal-alipay-qr" scope="worker" maxCount={1} />
</Form.Item>
</Form>
</Modal>
<Modal <Modal
title="提现申请" title="提现申请"
open={withdrawOpen} open={withdrawOpen}
@@ -310,6 +310,7 @@ export function saveAdminWorkerWithdrawalAccount(
payload: { payload: {
accountName: string accountName: string
accountNo?: string accountNo?: string
alipayQrCodeImages?: UploadedFile[]
wechatQrCodeImages?: UploadedFile[] wechatQrCodeImages?: UploadedFile[]
}, },
) { ) {
+6 -1
View File
@@ -1,4 +1,4 @@
import { apiDelete, apiGet, apiPost } from '@/lib/http' import { apiDelete, apiGet, apiPost, apiPut } from '@/lib/http'
import type { import type {
CollectLookupResponse, CollectLookupResponse,
UploadedFile, UploadedFile,
@@ -176,11 +176,16 @@ export function createWorkerWithdrawalAccount(payload: {
accountChannel: string accountChannel: string
accountName: string accountName: string
accountNo?: string accountNo?: string
alipayQrCodeImages?: UploadedFile[]
wechatQrCodeImages?: UploadedFile[] wechatQrCodeImages?: UploadedFile[]
}) { }) {
return apiPost('/api/v1/worker/profile/withdrawal-account', payload) return apiPost('/api/v1/worker/profile/withdrawal-account', payload)
} }
export function saveWorkerAlipayWithdrawalQrCode(payload: { alipayQrCodeImages: UploadedFile[] }) {
return apiPut('/api/v1/worker/profile/withdrawal-account/alipay-qr-code', payload)
}
export function changeWorkerPassword(payload: { currentPassword: string; newPassword: string }) { export function changeWorkerPassword(payload: { currentPassword: string; newPassword: string }) {
return apiPost<{ worker: WorkerUser; reloginRequired: boolean }>( return apiPost<{ worker: WorkerUser; reloginRequired: boolean }>(
'/api/v1/worker/profile/change-password', '/api/v1/worker/profile/change-password',
@@ -197,6 +197,7 @@ export type WorkerWithdrawalAccount = {
accountChannel: string accountChannel: string
accountName: string accountName: string
accountNoMasked: string accountNoMasked: string
alipayQrCodeImage: UploadedFile | null
wechatQrCodeImage: UploadedFile | null wechatQrCodeImage: UploadedFile | null
createdAt: string | null createdAt: string | null
} }
@@ -205,6 +206,7 @@ export type AdminWorkerWithdrawalAccount = {
accountChannel: string accountChannel: string
accountName: string accountName: string
accountNo: string accountNo: string
alipayQrCodeImage: UploadedFile | null
wechatQrCodeImage: UploadedFile | null wechatQrCodeImage: UploadedFile | null
createdAt: string createdAt: string
} }