优化打手提现审核流程
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
-- 打手提现账户:每位打手仅允许首次登记,后续变更由客服处理。
|
||||||
|
CREATE TABLE IF NOT EXISTS worker_withdrawal_accounts (
|
||||||
|
worker_id BIGINT PRIMARY KEY REFERENCES worker_users(id) ON DELETE CASCADE,
|
||||||
|
account_channel TEXT NOT NULL CHECK (account_channel IN ('alipay', 'wechat')),
|
||||||
|
account_name TEXT NOT NULL,
|
||||||
|
account_no TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE worker_withdrawal_accounts IS '打手不可自行修改的提现账户';
|
||||||
|
COMMENT ON COLUMN worker_withdrawal_accounts.account_channel IS '提现渠道:alipay / wechat';
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- 提现账户改为每个渠道各登记一次,保留既有账户数据。
|
||||||
|
ALTER TABLE worker_withdrawal_accounts
|
||||||
|
DROP CONSTRAINT IF EXISTS worker_withdrawal_accounts_pkey;
|
||||||
|
|
||||||
|
ALTER TABLE worker_withdrawal_accounts
|
||||||
|
ADD PRIMARY KEY (worker_id, account_channel);
|
||||||
|
|
||||||
|
COMMENT ON TABLE worker_withdrawal_accounts IS '打手每个渠道仅可自行登记一次的提现账户';
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- 微信提现不使用账号,改为保存不可修改的收款二维码。
|
||||||
|
ALTER TABLE worker_withdrawal_accounts
|
||||||
|
ADD COLUMN IF NOT EXISTS wechat_qr_code_json JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN worker_withdrawal_accounts.wechat_qr_code_json IS '微信收款二维码文件信息';
|
||||||
@@ -94,6 +94,15 @@ export type WorkerFinanceRequestRow = {
|
|||||||
worker_phone?: string
|
worker_phone?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkerWithdrawalAccountRow = {
|
||||||
|
worker_id: number
|
||||||
|
account_channel: string
|
||||||
|
account_name: string
|
||||||
|
account_no: string
|
||||||
|
wechat_qr_code_json: string | Record<string, unknown>
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
export type WorkCategoryRow = {
|
export type WorkCategoryRow = {
|
||||||
id: number
|
id: number
|
||||||
category_key: string
|
category_key: string
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
WorkerFinanceRequestRow,
|
WorkerFinanceRequestRow,
|
||||||
WorkerLevelRow,
|
WorkerLevelRow,
|
||||||
WorkerUserRow,
|
WorkerUserRow,
|
||||||
|
WorkerWithdrawalAccountRow,
|
||||||
WorkerWalletLedgerRow,
|
WorkerWalletLedgerRow,
|
||||||
WorkerWalletRow,
|
WorkerWalletRow,
|
||||||
} from './types.js'
|
} from './types.js'
|
||||||
@@ -437,6 +438,65 @@ export async function createWorkerFinanceRequest(input: {
|
|||||||
return result.rows[0] || null
|
return result.rows[0] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getWorkerWithdrawalAccount(
|
||||||
|
workerId: number | string,
|
||||||
|
accountChannel: string,
|
||||||
|
): Promise<WorkerWithdrawalAccountRow | null> {
|
||||||
|
const result = await query<WorkerWithdrawalAccountRow>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM worker_withdrawal_accounts
|
||||||
|
WHERE worker_id = $1 AND account_channel = $2
|
||||||
|
LIMIT 1
|
||||||
|
`,
|
||||||
|
[Number(workerId), accountChannel],
|
||||||
|
)
|
||||||
|
return result.rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listWorkerWithdrawalAccounts(
|
||||||
|
workerId: number | string,
|
||||||
|
): Promise<WorkerWithdrawalAccountRow[]> {
|
||||||
|
const result = await query<WorkerWithdrawalAccountRow>(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM worker_withdrawal_accounts
|
||||||
|
WHERE worker_id = $1
|
||||||
|
ORDER BY account_channel ASC
|
||||||
|
`,
|
||||||
|
[Number(workerId)],
|
||||||
|
)
|
||||||
|
return result.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createWorkerWithdrawalAccount(input: {
|
||||||
|
workerId: number
|
||||||
|
accountChannel: string
|
||||||
|
accountName: string
|
||||||
|
accountNo: 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)
|
||||||
|
ON CONFLICT (worker_id, account_channel) DO NOTHING
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[
|
||||||
|
input.workerId,
|
||||||
|
input.accountChannel,
|
||||||
|
input.accountName,
|
||||||
|
input.accountNo,
|
||||||
|
input.wechatQrCodeJson,
|
||||||
|
input.now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return result.rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
export async function getWorkerFinanceRequestById(
|
export async function getWorkerFinanceRequestById(
|
||||||
requestId: number | string,
|
requestId: number | string,
|
||||||
): Promise<WorkerFinanceRequestRow | null> {
|
): Promise<WorkerFinanceRequestRow | null> {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { uploadFileAsset } from '../services/file-storage/file-storage-service.j
|
|||||||
import {
|
import {
|
||||||
changeWorkerPassword,
|
changeWorkerPassword,
|
||||||
createWorkerRechargeRequest,
|
createWorkerRechargeRequest,
|
||||||
|
createWorkerWithdrawalAccount,
|
||||||
createWorkerWithdrawRequest,
|
createWorkerWithdrawRequest,
|
||||||
getWorkerProfile,
|
getWorkerProfile,
|
||||||
getWorkerSessionSummary,
|
getWorkerSessionSummary,
|
||||||
@@ -147,6 +148,19 @@ router.post(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/profile/withdrawal-account',
|
||||||
|
requireActiveWorker,
|
||||||
|
createRouteHandler(
|
||||||
|
(req) => createWorkerWithdrawalAccount(req.body || {}, getRequiredWorkerSession(req)),
|
||||||
|
{
|
||||||
|
successMessage: '提现信息已添加',
|
||||||
|
errorMessage: '添加提现信息失败',
|
||||||
|
scope: '[worker/profile/withdrawal-account]',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/profile/withdraw-requests',
|
'/profile/withdraw-requests',
|
||||||
requireActiveWorker,
|
requireActiveWorker,
|
||||||
|
|||||||
@@ -260,6 +260,11 @@ 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 wechatQrCodeImage = refreshUploadedFileUrls(payload.wechatQrCodeImage)
|
||||||
|
if (String(wechatQrCodeImage.url || '').trim()) {
|
||||||
|
payload.wechatQrCodeImage = wechatQrCodeImage
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
requestId: Number(request.id),
|
requestId: Number(request.id),
|
||||||
requestType: request.request_type || '',
|
requestType: request.request_type || '',
|
||||||
@@ -270,7 +275,7 @@ export function mapFinanceRequest(request: WorkerFinanceRequestRow | null | unde
|
|||||||
accountNo: request.account_no || '',
|
accountNo: request.account_no || '',
|
||||||
note: request.note || '',
|
note: request.note || '',
|
||||||
reviewedNote: request.reviewed_note || '',
|
reviewedNote: request.reviewed_note || '',
|
||||||
payload: safeParseJson(request.payload_json),
|
payload,
|
||||||
createdAt: request.created_at,
|
createdAt: request.created_at,
|
||||||
updatedAt: request.updated_at,
|
updatedAt: request.updated_at,
|
||||||
reviewedAt: request.reviewed_at,
|
reviewedAt: request.reviewed_at,
|
||||||
@@ -901,7 +906,7 @@ export function normalizeAdminFinanceReviewStatus(value: unknown) {
|
|||||||
|
|
||||||
export function normalizeWithdrawChannel(value: unknown) {
|
export function normalizeWithdrawChannel(value: unknown) {
|
||||||
const channel = String(value || '').trim()
|
const channel = String(value || '').trim()
|
||||||
if (['alipay', 'wechat', 'bank'].includes(channel)) {
|
if (['alipay', 'wechat'].includes(channel)) {
|
||||||
return channel
|
return channel
|
||||||
}
|
}
|
||||||
throw createHttpError('提现方式不正确', {
|
throw createHttpError('提现方式不正确', {
|
||||||
@@ -910,6 +915,16 @@ export function normalizeWithdrawChannel(value: unknown) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 提现金额超过 100 元时,仅允许使用支付宝。 */
|
||||||
|
export function assertWithdrawChannelAllowedForAmount(amount: number, channel: string) {
|
||||||
|
if (amount > 10_000 && channel !== 'alipay') {
|
||||||
|
throw createHttpError('提现金额超过 100 元时,仅支持支付宝提现', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_withdraw_channel_amount_limited',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeMatchType(value: unknown) {
|
export function normalizeMatchType(value: unknown) {
|
||||||
const matchType = String(value || '').trim()
|
const matchType = String(value || '').trim()
|
||||||
if (matchType === 'exact') return 'exact'
|
if (matchType === 'exact') return 'exact'
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import test from 'node:test'
|
|||||||
|
|
||||||
import type { WorkOrderRow, WorkerUserRow } from '../../repositories/worker-platform/index.js'
|
import type { WorkOrderRow, WorkerUserRow } from '../../repositories/worker-platform/index.js'
|
||||||
import {
|
import {
|
||||||
|
assertWithdrawChannelAllowedForAmount,
|
||||||
assertWorkerLoginAllowed,
|
assertWorkerLoginAllowed,
|
||||||
|
normalizeWithdrawChannel,
|
||||||
normalizeRequirementFields,
|
normalizeRequirementFields,
|
||||||
resolveCollectSubmitTargetWorkOrder,
|
resolveCollectSubmitTargetWorkOrder,
|
||||||
resolveSkuNameQuantity,
|
resolveSkuNameQuantity,
|
||||||
@@ -72,6 +74,28 @@ test('assertWorkerLoginAllowed blocks disabled workers with worker_disabled', ()
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('微信提现金额不超过 100 元时允许提交', () => {
|
||||||
|
assert.doesNotThrow(() => assertWithdrawChannelAllowedForAmount(10_000, 'wechat'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('微信提现金额超过 100 元时被拒绝', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => assertWithdrawChannelAllowedForAmount(10_001, 'wechat'),
|
||||||
|
(error: unknown) =>
|
||||||
|
Boolean(
|
||||||
|
error &&
|
||||||
|
typeof error === 'object' &&
|
||||||
|
(error as { errorCode?: string }).errorCode === 'worker_withdraw_channel_amount_limited',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('提现渠道仅支持支付宝和微信', () => {
|
||||||
|
assert.equal(normalizeWithdrawChannel('alipay'), 'alipay')
|
||||||
|
assert.equal(normalizeWithdrawChannel('wechat'), 'wechat')
|
||||||
|
assert.throws(() => normalizeWithdrawChannel('bank'))
|
||||||
|
})
|
||||||
|
|
||||||
test('normalizeRequirementFields default template keeps gameId/gameNickname as text inputs', () => {
|
test('normalizeRequirementFields default template keeps gameId/gameNickname as text inputs', () => {
|
||||||
const fields = normalizeRequirementFields([])
|
const fields = normalizeRequirementFields([])
|
||||||
const gameId = fields.find((field) => field.key === 'gameId')
|
const gameId = fields.find((field) => field.key === 'gameId')
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ import {
|
|||||||
createWorkOrderEvent,
|
createWorkOrderEvent,
|
||||||
createWorkerFinanceRequest,
|
createWorkerFinanceRequest,
|
||||||
createWorkerUser,
|
createWorkerUser,
|
||||||
|
createWorkerWithdrawalAccount as createWorkerWithdrawalAccountRecord,
|
||||||
getWorkerFinanceRequestSummary,
|
getWorkerFinanceRequestSummary,
|
||||||
getWorkCategoryByKey,
|
getWorkCategoryByKey,
|
||||||
getWorkerLevelByKey,
|
getWorkerLevelByKey,
|
||||||
getLatestWorkerSmsCode,
|
getLatestWorkerSmsCode,
|
||||||
getWorkerUserByInviteCode,
|
getWorkerUserByInviteCode,
|
||||||
getWorkerUserById,
|
getWorkerUserById,
|
||||||
|
getWorkerWithdrawalAccount,
|
||||||
getWorkerUserByPhone,
|
getWorkerUserByPhone,
|
||||||
getWorkerUserByUsername,
|
getWorkerUserByUsername,
|
||||||
getWorkerUserByDisplayName,
|
getWorkerUserByDisplayName,
|
||||||
@@ -35,6 +37,7 @@ import {
|
|||||||
listWorkerSharesByWorker,
|
listWorkerSharesByWorker,
|
||||||
listWorkerWalletLedgers,
|
listWorkerWalletLedgers,
|
||||||
listWorkerWorkOrderNotes,
|
listWorkerWorkOrderNotes,
|
||||||
|
listWorkerWithdrawalAccounts,
|
||||||
listWorkCategories,
|
listWorkCategories,
|
||||||
listWorkOrders,
|
listWorkOrders,
|
||||||
listWorkOrderEventsByOrderId,
|
listWorkOrderEventsByOrderId,
|
||||||
@@ -63,6 +66,7 @@ import { nowIso } from '../../utils/time.js'
|
|||||||
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
|
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
|
||||||
import { getSmsProvider } from '../sms/index.js'
|
import { getSmsProvider } from '../sms/index.js'
|
||||||
import { getWorkerFinanceConfig } from './worker-finance-config-service.js'
|
import { getWorkerFinanceConfig } from './worker-finance-config-service.js'
|
||||||
|
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_CATEGORY_KEY,
|
DEFAULT_CATEGORY_KEY,
|
||||||
@@ -84,6 +88,7 @@ import {
|
|||||||
mapWorkOrderShare,
|
mapWorkOrderShare,
|
||||||
mapWorkerUser,
|
mapWorkerUser,
|
||||||
normalizeAmountFen,
|
normalizeAmountFen,
|
||||||
|
assertWithdrawChannelAllowedForAmount,
|
||||||
normalizeFinanceRequestStatus,
|
normalizeFinanceRequestStatus,
|
||||||
normalizeFinanceRequestType,
|
normalizeFinanceRequestType,
|
||||||
normalizeInteger,
|
normalizeInteger,
|
||||||
@@ -109,6 +114,8 @@ import {
|
|||||||
verifyWorkerPassword,
|
verifyWorkerPassword,
|
||||||
} from './mappers.js'
|
} from './mappers.js'
|
||||||
|
|
||||||
|
const WORKER_DAILY_WITHDRAW_LIMIT = 3
|
||||||
|
|
||||||
export type WorkerSession = {
|
export type WorkerSession = {
|
||||||
sessionId: string
|
sessionId: string
|
||||||
workerId: number
|
workerId: number
|
||||||
@@ -505,13 +512,20 @@ export function requireActiveWorkerSession(session: WorkerSession | null | undef
|
|||||||
|
|
||||||
export async function getWorkerProfile(session: WorkerSession) {
|
export async function getWorkerProfile(session: WorkerSession) {
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
const [financeSummary, acceptedOrderCount, activeOrderCount, timeoutOrderCount, financeConfig] =
|
const [
|
||||||
await Promise.all([
|
financeSummary,
|
||||||
|
acceptedOrderCount,
|
||||||
|
activeOrderCount,
|
||||||
|
timeoutOrderCount,
|
||||||
|
financeConfig,
|
||||||
|
withdrawalAccounts,
|
||||||
|
] = await Promise.all([
|
||||||
getWorkerFinanceRequestSummary(session.workerId),
|
getWorkerFinanceRequestSummary(session.workerId),
|
||||||
countWorkerAcceptedOrders(session.workerId),
|
countWorkerAcceptedOrders(session.workerId),
|
||||||
countWorkerActiveOrders(session.workerId),
|
countWorkerActiveOrders(session.workerId),
|
||||||
countWorkerTimeoutEvents(session.workerId),
|
countWorkerTimeoutEvents(session.workerId),
|
||||||
Promise.resolve(getWorkerFinanceConfig()),
|
Promise.resolve(getWorkerFinanceConfig()),
|
||||||
|
listWorkerWithdrawalAccounts(session.workerId),
|
||||||
])
|
])
|
||||||
const permissions = resolveWorkerPermissions(worker)
|
const permissions = resolveWorkerPermissions(worker)
|
||||||
const levelProgress = await resolveLevelProgress(worker, acceptedOrderCount)
|
const levelProgress = await resolveLevelProgress(worker, acceptedOrderCount)
|
||||||
@@ -528,6 +542,7 @@ export async function getWorkerProfile(session: WorkerSession) {
|
|||||||
},
|
},
|
||||||
levelProgress,
|
levelProgress,
|
||||||
financeConfig,
|
financeConfig,
|
||||||
|
withdrawalAccounts: withdrawalAccounts.map(mapWithdrawalAccount),
|
||||||
orderTimelineVisible: runtimeConfig.worker.orderTimelineVisible === true,
|
orderTimelineVisible: runtimeConfig.worker.orderTimelineVisible === true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -696,8 +711,8 @@ export async function createWorkerWithdrawRequest(
|
|||||||
resolveChinaDayRange().start,
|
resolveChinaDayRange().start,
|
||||||
resolveChinaDayRange().end,
|
resolveChinaDayRange().end,
|
||||||
)
|
)
|
||||||
if (withdrawCountToday > 0) {
|
if (withdrawCountToday >= WORKER_DAILY_WITHDRAW_LIMIT) {
|
||||||
throw createHttpError('每天限提现 1 次,今天已提交过提现申请', {
|
throw createHttpError('每天最多提现 3 次(支付宝、微信共享次数),今天已达上限', {
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
errorCode: 'worker_withdraw_daily_limit_reached',
|
errorCode: 'worker_withdraw_daily_limit_reached',
|
||||||
})
|
})
|
||||||
@@ -712,18 +727,19 @@ export async function createWorkerWithdrawRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const accountChannel = normalizeWithdrawChannel(payload.accountChannel || payload.channel)
|
const accountChannel = normalizeWithdrawChannel(payload.accountChannel || payload.channel)
|
||||||
const accountName = String(payload.accountName || payload.realName || '').trim()
|
const withdrawalAccount = await getWorkerWithdrawalAccount(worker.id, accountChannel)
|
||||||
const accountNo = String(payload.accountNo || payload.account || '').trim()
|
if (!withdrawalAccount) {
|
||||||
if (!accountName) {
|
throw createHttpError('请先添加对应的提现信息;修改提现信息请联系客服处理', {
|
||||||
throw createHttpError('请填写收款人姓名', {
|
statusCode: 409,
|
||||||
statusCode: 400,
|
errorCode: 'worker_withdrawal_account_required',
|
||||||
errorCode: 'worker_withdraw_account_name_required',
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (!accountNo) {
|
assertWithdrawChannelAllowedForAmount(amount, accountChannel)
|
||||||
throw createHttpError('请填写收款账号', {
|
const wechatQrCodeImage = resolveWechatWithdrawalQrCode(withdrawalAccount)
|
||||||
statusCode: 400,
|
if (accountChannel === 'wechat' && !wechatQrCodeImage) {
|
||||||
errorCode: 'worker_withdraw_account_no_required',
|
throw createHttpError('微信提现信息缺少收款二维码,请联系客服处理', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_withdrawal_wechat_qr_required',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -743,13 +759,14 @@ export async function createWorkerWithdrawRequest(
|
|||||||
requestType: 'withdraw',
|
requestType: 'withdraw',
|
||||||
amount,
|
amount,
|
||||||
accountChannel,
|
accountChannel,
|
||||||
accountName,
|
accountName: withdrawalAccount.account_name,
|
||||||
accountNo,
|
accountNo: withdrawalAccount.account_no,
|
||||||
note: note || '个人中心提现申请',
|
note: note || '个人中心提现申请',
|
||||||
payloadJson: JSON.stringify({
|
payloadJson: JSON.stringify({
|
||||||
source: 'worker_profile_withdraw',
|
source: 'worker_profile_withdraw',
|
||||||
username: worker.username,
|
username: worker.username,
|
||||||
phone: worker.phone || '',
|
phone: worker.phone || '',
|
||||||
|
wechatQrCodeImage,
|
||||||
}),
|
}),
|
||||||
now: nowIso(),
|
now: nowIso(),
|
||||||
})
|
})
|
||||||
@@ -765,6 +782,97 @@ export async function createWorkerWithdrawRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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 === 'wechat' && !wechatQrCodeImage) {
|
||||||
|
throw createHttpError('请上传微信收款二维码', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_withdraw_wechat_qr_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await createWorkerWithdrawalAccountRecord({
|
||||||
|
workerId: worker.id,
|
||||||
|
accountChannel,
|
||||||
|
accountName,
|
||||||
|
accountNo,
|
||||||
|
wechatQrCodeJson: JSON.stringify(wechatQrCodeImage || {}),
|
||||||
|
now: nowIso(),
|
||||||
|
})
|
||||||
|
if (!created) {
|
||||||
|
throw createHttpError('该提现方式的信息已添加,修改请联系客服处理', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_withdrawal_account_already_set',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
withdrawalAccount: mapWithdrawalAccount(created),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapWithdrawalAccount(account: {
|
||||||
|
account_channel: string
|
||||||
|
account_name: string
|
||||||
|
account_no: string
|
||||||
|
wechat_qr_code_json: string | Record<string, unknown>
|
||||||
|
created_at: string
|
||||||
|
}) {
|
||||||
|
const wechatQrCodeImage = resolveWechatWithdrawalQrCode(account)
|
||||||
|
return {
|
||||||
|
accountChannel: account.account_channel,
|
||||||
|
accountName: account.account_name,
|
||||||
|
accountNoMasked:
|
||||||
|
account.account_channel === 'alipay' ? maskWithdrawalAccountNo(account.account_no) : '',
|
||||||
|
wechatQrCodeImage,
|
||||||
|
createdAt: account.created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveWechatWithdrawalQrCode(account: {
|
||||||
|
wechat_qr_code_json: string | Record<string, unknown>
|
||||||
|
}) {
|
||||||
|
const file = refreshUploadedFileUrls(safeParseJson(account.wechat_qr_code_json))
|
||||||
|
return String(file.url || '').trim() ? file : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function maskWithdrawalAccountNo(accountNo: string) {
|
||||||
|
const normalized = String(accountNo || '').trim()
|
||||||
|
if (normalized.length <= 4) return normalized
|
||||||
|
return `${'*'.repeat(Math.max(4, normalized.length - 4))}${normalized.slice(-4)}`
|
||||||
|
}
|
||||||
|
|
||||||
export async function changeWorkerPassword(payload: JsonObject = {}, session: WorkerSession) {
|
export async function changeWorkerPassword(payload: JsonObject = {}, session: WorkerSession) {
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
const currentPassword = normalizePassword(payload.currentPassword || payload.oldPassword)
|
const currentPassword = normalizePassword(payload.currentPassword || payload.oldPassword)
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export default function FinancePanel() {
|
|||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
||||||
const [savingConfig, setSavingConfig] = useState(false)
|
const [savingConfig, setSavingConfig] = useState(false)
|
||||||
|
const [configExpanded, setConfigExpanded] = useState(false)
|
||||||
const [reviewing, setReviewing] = useState(false)
|
const [reviewing, setReviewing] = useState(false)
|
||||||
const [reviewState, setReviewState] = useState<{
|
const [reviewState, setReviewState] = useState<{
|
||||||
action: FinanceReviewAction
|
action: FinanceReviewAction
|
||||||
@@ -207,13 +208,15 @@ export default function FinancePanel() {
|
|||||||
render: (_, row) => renderFinanceRequestAccount(row),
|
render: (_, row) => renderFinanceRequestAccount(row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '付款凭证',
|
title: '凭证/收款码',
|
||||||
minWidth: 140,
|
minWidth: 140,
|
||||||
render: (_, row) =>
|
render: (_, row) => (
|
||||||
row.requestType === 'recharge' ? (
|
<ImagePreviewList
|
||||||
<ImagePreviewList files={getRechargeProofs(row)} size={48} />
|
files={
|
||||||
) : (
|
row.requestType === 'recharge' ? getRechargeProofs(row) : getWechatWithdrawQrCode(row)
|
||||||
'-'
|
}
|
||||||
|
size={48}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -276,6 +279,10 @@ export default function FinancePanel() {
|
|||||||
bordered={false}
|
bordered={false}
|
||||||
loading={financeConfigQuery.isLoading}
|
loading={financeConfigQuery.isLoading}
|
||||||
extra={
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Button type="link" onClick={() => setConfigExpanded((expanded) => !expanded)}>
|
||||||
|
{configExpanded ? '收起配置' : '展开配置'}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
loading={financeConfigQuery.isFetching}
|
loading={financeConfigQuery.isFetching}
|
||||||
@@ -283,15 +290,17 @@ export default function FinancePanel() {
|
|||||||
>
|
>
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{financeConfigQuery.error ? (
|
{configExpanded && financeConfigQuery.error ? (
|
||||||
<Typography.Text type="danger">
|
<Typography.Text type="danger">
|
||||||
{financeConfigQuery.error instanceof Error
|
{financeConfigQuery.error instanceof Error
|
||||||
? financeConfigQuery.error.message
|
? financeConfigQuery.error.message
|
||||||
: '读取资金配置失败'}
|
: '读取资金配置失败'}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
{configExpanded ? (
|
||||||
<Form
|
<Form
|
||||||
form={configForm}
|
form={configForm}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
@@ -299,7 +308,11 @@ export default function FinancePanel() {
|
|||||||
initialValues={mapFinanceConfigToFormValues()}
|
initialValues={mapFinanceConfigToFormValues()}
|
||||||
>
|
>
|
||||||
<Card title="充值入口" size="small" style={{ marginBottom: 16 }}>
|
<Card title="充值入口" size="small" style={{ marginBottom: 16 }}>
|
||||||
<Form.Item label="开启充值申请" name={['recharge', 'enabled']} valuePropName="checked">
|
<Form.Item
|
||||||
|
label="开启充值申请"
|
||||||
|
name={['recharge', 'enabled']}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="收款通道名称" name={['recharge', 'channelName']}>
|
<Form.Item label="收款通道名称" name={['recharge', 'channelName']}>
|
||||||
@@ -342,7 +355,11 @@ export default function FinancePanel() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card title="提现入口" size="small" style={{ marginBottom: 16 }}>
|
<Card title="提现入口" size="small" style={{ marginBottom: 16 }}>
|
||||||
<Form.Item label="开启提现申请" name={['withdraw', 'enabled']} valuePropName="checked">
|
<Form.Item
|
||||||
|
label="开启提现申请"
|
||||||
|
name={['withdraw', 'enabled']}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="提现说明" name={['withdraw', 'instructions']}>
|
<Form.Item label="提现说明" name={['withdraw', 'instructions']}>
|
||||||
@@ -357,6 +374,7 @@ export default function FinancePanel() {
|
|||||||
保存配置
|
保存配置
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
|
) : null}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
@@ -461,6 +479,16 @@ export default function FinancePanel() {
|
|||||||
<Descriptions.Item label="收款信息">
|
<Descriptions.Item label="收款信息">
|
||||||
{renderFinanceRequestAccountText(reviewState.request)}
|
{renderFinanceRequestAccountText(reviewState.request)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
{reviewState.request.requestType === 'withdraw' &&
|
||||||
|
reviewState.request.accountChannel === 'alipay' ? (
|
||||||
|
<Descriptions.Item label="支付宝账号">
|
||||||
|
{reviewState.request.accountNo ? (
|
||||||
|
<Typography.Text copyable>{reviewState.request.accountNo}</Typography.Text>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
) : null}
|
||||||
{reviewState.request.requestType === 'recharge' ? (
|
{reviewState.request.requestType === 'recharge' ? (
|
||||||
<>
|
<>
|
||||||
<Descriptions.Item label="付款时间">
|
<Descriptions.Item label="付款时间">
|
||||||
@@ -473,6 +501,15 @@ export default function FinancePanel() {
|
|||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
{reviewState.request.requestType === 'withdraw' &&
|
||||||
|
reviewState.request.accountChannel === 'wechat' ? (
|
||||||
|
<Descriptions.Item label="微信收款二维码">
|
||||||
|
<ImagePreviewList
|
||||||
|
files={getWechatWithdrawQrCode(reviewState.request)}
|
||||||
|
size={88}
|
||||||
|
/>
|
||||||
|
</Descriptions.Item>
|
||||||
|
) : null}
|
||||||
<Descriptions.Item label="申请备注">
|
<Descriptions.Item label="申请备注">
|
||||||
{reviewState.request.note || '-'}
|
{reviewState.request.note || '-'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
@@ -551,7 +588,7 @@ function renderFinanceRequestAccount(request: WorkerFinanceRequest) {
|
|||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
<Typography.Text>{renderFinanceRequestAccountText(request)}</Typography.Text>
|
<Typography.Text>{renderFinanceRequestAccountText(request)}</Typography.Text>
|
||||||
{request.requestType === 'withdraw' && request.accountNo ? (
|
{request.requestType === 'withdraw' && request.accountNo ? (
|
||||||
<Typography.Text type="secondary">{maskFinanceAccount(request.accountNo)}</Typography.Text>
|
<Typography.Text copyable>{request.accountNo}</Typography.Text>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -606,6 +643,25 @@ function getRechargeProofs(request: WorkerFinanceRequest): UploadedFile[] {
|
|||||||
.filter((item): item is UploadedFile => Boolean(item))
|
.filter((item): item is UploadedFile => Boolean(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWechatWithdrawQrCode(request: WorkerFinanceRequest): UploadedFile[] {
|
||||||
|
if (request.requestType !== 'withdraw' || request.accountChannel !== 'wechat') return []
|
||||||
|
const payload = asRecord(request.payload)
|
||||||
|
const file = asRecord(payload.wechatQrCodeImage)
|
||||||
|
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()
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import ImageUpload from '@/components/files/ImageUpload'
|
|||||||
import {
|
import {
|
||||||
changeWorkerPassword,
|
changeWorkerPassword,
|
||||||
createWorkerRechargeRequest,
|
createWorkerRechargeRequest,
|
||||||
|
createWorkerWithdrawalAccount,
|
||||||
createWorkerWithdrawRequest,
|
createWorkerWithdrawRequest,
|
||||||
fetchWorkerFinanceRequests,
|
fetchWorkerFinanceRequests,
|
||||||
fetchWorkerProfile,
|
fetchWorkerProfile,
|
||||||
@@ -66,10 +67,15 @@ type RechargeFormValues = {
|
|||||||
|
|
||||||
type WithdrawFormValues = {
|
type WithdrawFormValues = {
|
||||||
amount?: number
|
amount?: number
|
||||||
|
accountChannel?: string
|
||||||
|
note?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WithdrawalAccountFormValues = {
|
||||||
accountChannel?: string
|
accountChannel?: string
|
||||||
accountName?: string
|
accountName?: string
|
||||||
accountNo?: string
|
accountNo?: string
|
||||||
note?: string
|
wechatQrCodeImages?: UploadedFile[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type PasswordFormValues = {
|
type PasswordFormValues = {
|
||||||
@@ -94,14 +100,19 @@ export default function WorkerProfilePage() {
|
|||||||
const [requestStatus, setRequestStatus] = useState('')
|
const [requestStatus, setRequestStatus] = useState('')
|
||||||
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 [passwordOpen, setPasswordOpen] = useState(false)
|
const [passwordOpen, setPasswordOpen] = useState(false)
|
||||||
const [contactOpen, setContactOpen] = useState(false)
|
const [contactOpen, setContactOpen] = useState(false)
|
||||||
const [submittingRecharge, setSubmittingRecharge] = useState(false)
|
const [submittingRecharge, setSubmittingRecharge] = useState(false)
|
||||||
const [submittingWithdraw, setSubmittingWithdraw] = useState(false)
|
const [submittingWithdraw, setSubmittingWithdraw] = useState(false)
|
||||||
|
const [submittingWithdrawalAccount, setSubmittingWithdrawalAccount] = useState(false)
|
||||||
const [submittingPassword, setSubmittingPassword] = useState(false)
|
const [submittingPassword, setSubmittingPassword] = useState(false)
|
||||||
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 [passwordForm] = Form.useForm<PasswordFormValues>()
|
const [passwordForm] = Form.useForm<PasswordFormValues>()
|
||||||
|
const selectedWithdrawChannel = Form.useWatch('accountChannel', withdrawForm)
|
||||||
|
const selectedWithdrawalAccountChannel = Form.useWatch('accountChannel', withdrawalAccountForm)
|
||||||
|
|
||||||
const profileQuery = useQuery({
|
const profileQuery = useQuery({
|
||||||
queryKey: ['worker-profile'],
|
queryKey: ['worker-profile'],
|
||||||
@@ -113,6 +124,10 @@ export default function WorkerProfilePage() {
|
|||||||
const summary = profileQuery.data?.data.summary
|
const summary = profileQuery.data?.data.summary
|
||||||
const levelProgress = profileQuery.data?.data.levelProgress
|
const levelProgress = profileQuery.data?.data.levelProgress
|
||||||
const financeConfig = profileQuery.data?.data.financeConfig
|
const financeConfig = profileQuery.data?.data.financeConfig
|
||||||
|
const withdrawalAccounts = profileQuery.data?.data.withdrawalAccounts || []
|
||||||
|
const selectedWithdrawalAccount = withdrawalAccounts.find(
|
||||||
|
(account) => account.accountChannel === selectedWithdrawChannel,
|
||||||
|
)
|
||||||
const canUseFinanceActions = worker?.status === 'active'
|
const canUseFinanceActions = worker?.status === 'active'
|
||||||
|
|
||||||
async function copyInviteCode() {
|
async function copyInviteCode() {
|
||||||
@@ -191,9 +206,26 @@ export default function WorkerProfilePage() {
|
|||||||
message.warning('当前提现入口暂未开放,请联系管理员')
|
message.warning('当前提现入口暂未开放,请联系管理员')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (withdrawalAccounts.length === 0) {
|
||||||
|
openWithdrawalAccountModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
withdrawForm.setFieldValue('accountChannel', withdrawalAccounts[0].accountChannel)
|
||||||
setWithdrawOpen(true)
|
setWithdrawOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openWithdrawalAccountModal() {
|
||||||
|
const nextChannel = ['alipay', 'wechat'].find(
|
||||||
|
(channel) => !withdrawalAccounts.some((account) => account.accountChannel === channel),
|
||||||
|
)
|
||||||
|
if (!nextChannel) {
|
||||||
|
message.info('支付宝和微信提现信息均已添加,修改请联系客服处理')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
withdrawalAccountForm.setFieldValue('accountChannel', nextChannel)
|
||||||
|
setWithdrawalAccountOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
async function submitRecharge(values: RechargeFormValues) {
|
async function submitRecharge(values: RechargeFormValues) {
|
||||||
setSubmittingRecharge(true)
|
setSubmittingRecharge(true)
|
||||||
try {
|
try {
|
||||||
@@ -222,9 +254,7 @@ export default function WorkerProfilePage() {
|
|||||||
try {
|
try {
|
||||||
await createWorkerWithdrawRequest({
|
await createWorkerWithdrawRequest({
|
||||||
amount: values.amount,
|
amount: values.amount,
|
||||||
accountChannel: values.accountChannel,
|
accountChannel: String(values.accountChannel || ''),
|
||||||
accountName: String(values.accountName || '').trim(),
|
|
||||||
accountNo: String(values.accountNo || '').trim(),
|
|
||||||
note: String(values.note || '').trim(),
|
note: String(values.note || '').trim(),
|
||||||
})
|
})
|
||||||
message.success('提现申请已提交,请等待管理员审核')
|
message.success('提现申请已提交,请等待管理员审核')
|
||||||
@@ -241,6 +271,28 @@ export default function WorkerProfilePage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitWithdrawalAccount(values: WithdrawalAccountFormValues) {
|
||||||
|
setSubmittingWithdrawalAccount(true)
|
||||||
|
try {
|
||||||
|
await createWorkerWithdrawalAccount({
|
||||||
|
accountChannel: String(values.accountChannel || ''),
|
||||||
|
accountName: String(values.accountName || '').trim(),
|
||||||
|
accountNo: String(values.accountNo || '').trim(),
|
||||||
|
wechatQrCodeImages: values.wechatQrCodeImages || [],
|
||||||
|
})
|
||||||
|
message.success('提现信息已添加,该方式后续修改请联系客服处理')
|
||||||
|
setWithdrawalAccountOpen(false)
|
||||||
|
withdrawalAccountForm.resetFields()
|
||||||
|
await refreshAll()
|
||||||
|
withdrawForm.setFieldValue('accountChannel', String(values.accountChannel || ''))
|
||||||
|
setWithdrawOpen(true)
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '添加提现信息失败')
|
||||||
|
} finally {
|
||||||
|
setSubmittingWithdrawalAccount(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submitPasswordChange(values: PasswordFormValues) {
|
async function submitPasswordChange(values: PasswordFormValues) {
|
||||||
setSubmittingPassword(true)
|
setSubmittingPassword(true)
|
||||||
try {
|
try {
|
||||||
@@ -1055,6 +1107,15 @@ export default function WorkerProfilePage() {
|
|||||||
>
|
>
|
||||||
提现申请
|
提现申请
|
||||||
</Button>
|
</Button>
|
||||||
|
{withdrawalAccounts.length < 2 ? (
|
||||||
|
<Button
|
||||||
|
icon={<BankOutlined />}
|
||||||
|
disabled={!canUseFinanceActions}
|
||||||
|
onClick={openWithdrawalAccountModal}
|
||||||
|
>
|
||||||
|
添加提现信息
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
<Button icon={<LockOutlined />} onClick={() => setPasswordOpen(true)}>
|
<Button icon={<LockOutlined />} onClick={() => setPasswordOpen(true)}>
|
||||||
修改密码
|
修改密码
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1122,6 +1183,37 @@ export default function WorkerProfilePage() {
|
|||||||
<Descriptions.Item label="提现中金额">
|
<Descriptions.Item label="提现中金额">
|
||||||
{formatMoney(summary?.pendingWithdrawAmount)}
|
{formatMoney(summary?.pendingWithdrawAmount)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="提现信息" span={2}>
|
||||||
|
{withdrawalAccounts.length > 0 ? (
|
||||||
|
<Space direction="vertical" size={2}>
|
||||||
|
{withdrawalAccounts.map((account) => (
|
||||||
|
<Space key={account.accountChannel} wrap size={4}>
|
||||||
|
<Typography.Text>
|
||||||
|
{formatWithdrawChannel(account.accountChannel)}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text>{account.accountName}</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{account.accountChannel === 'wechat'
|
||||||
|
? '已上传收款二维码'
|
||||||
|
: account.accountNoMasked}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
已锁定,修改请联系客服
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
disabled={!canUseFinanceActions}
|
||||||
|
onClick={openWithdrawalAccountModal}
|
||||||
|
>
|
||||||
|
添加提现信息
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="免押额度">
|
<Descriptions.Item label="免押额度">
|
||||||
{formatMoney(worker.level?.permissions.depositFreeAmount || 0)}
|
{formatMoney(worker.level?.permissions.depositFreeAmount || 0)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
@@ -1387,6 +1479,69 @@ export default function WorkerProfilePage() {
|
|||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="添加提现信息"
|
||||||
|
open={withdrawalAccountOpen}
|
||||||
|
width={isMobile ? '92%' : 500}
|
||||||
|
destroyOnHidden
|
||||||
|
confirmLoading={submittingWithdrawalAccount}
|
||||||
|
onCancel={() => {
|
||||||
|
setWithdrawalAccountOpen(false)
|
||||||
|
withdrawalAccountForm.resetFields()
|
||||||
|
}}
|
||||||
|
onOk={() => withdrawalAccountForm.submit()}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
showIcon
|
||||||
|
type="warning"
|
||||||
|
message="支付宝和微信可分别添加一次;每种方式添加后均无法自行修改,如需修改请联系客服。"
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
<Form form={withdrawalAccountForm} layout="vertical" onFinish={submitWithdrawalAccount}>
|
||||||
|
<Form.Item
|
||||||
|
label="提现方式"
|
||||||
|
name="accountChannel"
|
||||||
|
rules={[{ required: true, message: '请选择提现方式' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
...(!withdrawalAccounts.some((account) => account.accountChannel === 'alipay')
|
||||||
|
? [{ value: 'alipay', label: '支付宝' }]
|
||||||
|
: []),
|
||||||
|
...(!withdrawalAccounts.some((account) => account.accountChannel === 'wechat')
|
||||||
|
? [{ value: 'wechat', label: '微信收款(仅支持 100 元及以下提现)' }]
|
||||||
|
: []),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="收款人姓名"
|
||||||
|
name="accountName"
|
||||||
|
rules={[{ required: true, message: '请输入收款人姓名' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入真实姓名" />
|
||||||
|
</Form.Item>
|
||||||
|
{selectedWithdrawalAccountChannel === 'wechat' ? (
|
||||||
|
<Form.Item
|
||||||
|
label="微信收款二维码"
|
||||||
|
name="wechatQrCodeImages"
|
||||||
|
rules={[{ required: true, message: '请上传微信收款二维码' }]}
|
||||||
|
extra="微信提现仅使用收款二维码,不需要填写微信账号。"
|
||||||
|
>
|
||||||
|
<ImageUpload scene="worker-withdrawal-wechat-qr" scope="worker" maxCount={1} />
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<Form.Item
|
||||||
|
label="支付宝账号"
|
||||||
|
name="accountNo"
|
||||||
|
rules={[{ required: true, message: '请输入支付宝账号' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入支付宝账号" />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title="提现申请"
|
title="提现申请"
|
||||||
open={withdrawOpen}
|
open={withdrawOpen}
|
||||||
@@ -1399,12 +1554,7 @@ export default function WorkerProfilePage() {
|
|||||||
}}
|
}}
|
||||||
onOk={() => withdrawForm.submit()}
|
onOk={() => withdrawForm.submit()}
|
||||||
>
|
>
|
||||||
<Form
|
<Form form={withdrawForm} layout="vertical" onFinish={submitWithdraw}>
|
||||||
form={withdrawForm}
|
|
||||||
layout="vertical"
|
|
||||||
initialValues={{ accountChannel: 'alipay' }}
|
|
||||||
onFinish={submitWithdraw}
|
|
||||||
>
|
|
||||||
{financeConfig?.withdraw.instructions ? (
|
{financeConfig?.withdraw.instructions ? (
|
||||||
<Alert
|
<Alert
|
||||||
showIcon
|
showIcon
|
||||||
@@ -1416,49 +1566,58 @@ export default function WorkerProfilePage() {
|
|||||||
<Alert
|
<Alert
|
||||||
showIcon
|
showIcon
|
||||||
type="warning"
|
type="warning"
|
||||||
message={`每天限提现 1 次;有押金的工单验收通过后,押金需 ${Number(
|
message={`每天最多提现 3 次(支付宝、微信共享次数);100 元及以下支持微信和支付宝,超过 100 元仅支持支付宝;有押金的工单验收通过后,押金需 ${Number(
|
||||||
financeConfig?.depositUnfreezeDays ?? 3,
|
financeConfig?.depositUnfreezeDays ?? 3,
|
||||||
)} 天解冻到账后方可提现。`}
|
)} 天解冻到账后方可提现。`}
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
/>
|
/>
|
||||||
|
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
|
||||||
|
<Descriptions.Item label="提现账户">
|
||||||
|
{formatWithdrawChannel(selectedWithdrawalAccount?.accountChannel || '')} ·{' '}
|
||||||
|
{selectedWithdrawalAccount?.accountName || '-'} ·{' '}
|
||||||
|
{selectedWithdrawalAccount?.accountChannel === 'wechat'
|
||||||
|
? '收款二维码'
|
||||||
|
: selectedWithdrawalAccount?.accountNoMasked || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
<Form.Item label="可提现余额">
|
<Form.Item label="可提现余额">
|
||||||
<Typography.Text strong>
|
<Typography.Text strong>
|
||||||
{formatMoney(resolveAvailableForWithdraw(worker, summary))}
|
{formatMoney(resolveAvailableForWithdraw(worker, summary))}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
|
||||||
label="提现金额"
|
|
||||||
name="amount"
|
|
||||||
rules={[{ required: true, message: '请输入提现金额' }]}
|
|
||||||
>
|
|
||||||
<InputNumber min={0.01} step={1} addonAfter="元" className="full-width" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="提现方式"
|
label="提现方式"
|
||||||
name="accountChannel"
|
name="accountChannel"
|
||||||
rules={[{ required: true, message: '请选择提现方式' }]}
|
rules={[{ required: true, message: '请选择提现方式' }]}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={withdrawalAccounts.map((account) => ({
|
||||||
{ value: 'alipay', label: '支付宝' },
|
value: account.accountChannel,
|
||||||
{ value: 'wechat', label: '微信收款' },
|
label:
|
||||||
{ value: 'bank', label: '银行卡' },
|
account.accountChannel === 'wechat'
|
||||||
]}
|
? '微信收款 · 收款二维码'
|
||||||
|
: `${formatWithdrawChannel(account.accountChannel)} · ${account.accountNoMasked}`,
|
||||||
|
}))}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="收款人姓名"
|
label="提现金额"
|
||||||
name="accountName"
|
name="amount"
|
||||||
rules={[{ required: true, message: '请输入收款人姓名' }]}
|
rules={[
|
||||||
|
{ required: true, message: '请输入提现金额' },
|
||||||
|
{
|
||||||
|
validator(_, value) {
|
||||||
|
if (Number(value || 0) > 100 && selectedWithdrawChannel !== 'alipay') {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error('超过 100 元仅支持支付宝提现,请联系客服修改提现信息'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="请输入真实姓名" />
|
<InputNumber min={0.01} step={1} addonAfter="元" className="full-width" />
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label="收款账号"
|
|
||||||
name="accountNo"
|
|
||||||
rules={[{ required: true, message: '请输入收款账号' }]}
|
|
||||||
>
|
|
||||||
<Input placeholder="请输入支付宝账号、微信号或银行卡号" />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="备注说明" name="note">
|
<Form.Item label="备注说明" name="note">
|
||||||
<Input.TextArea rows={4} placeholder="可补充到账要求、手机号、开户行等说明。" />
|
<Input.TextArea rows={4} placeholder="可补充到账要求、手机号、开户行等说明。" />
|
||||||
|
|||||||
@@ -83,9 +83,7 @@ export function createWorkerRechargeRequest(payload: {
|
|||||||
|
|
||||||
export function createWorkerWithdrawRequest(payload: {
|
export function createWorkerWithdrawRequest(payload: {
|
||||||
amount?: number
|
amount?: number
|
||||||
accountChannel?: string
|
accountChannel: string
|
||||||
accountName?: string
|
|
||||||
accountNo?: string
|
|
||||||
note?: string
|
note?: string
|
||||||
}) {
|
}) {
|
||||||
return apiPost<{ request: WorkerFinanceRequest }>(
|
return apiPost<{ request: WorkerFinanceRequest }>(
|
||||||
@@ -94,6 +92,15 @@ export function createWorkerWithdrawRequest(payload: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createWorkerWithdrawalAccount(payload: {
|
||||||
|
accountChannel: string
|
||||||
|
accountName: string
|
||||||
|
accountNo?: string
|
||||||
|
wechatQrCodeImages?: UploadedFile[]
|
||||||
|
}) {
|
||||||
|
return apiPost('/api/v1/worker/profile/withdrawal-account', 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',
|
||||||
|
|||||||
@@ -105,12 +105,21 @@ export type WorkerFinanceConfig = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorkerWithdrawalAccount = {
|
||||||
|
accountChannel: string
|
||||||
|
accountName: string
|
||||||
|
accountNoMasked: string
|
||||||
|
wechatQrCodeImage: UploadedFile | null
|
||||||
|
createdAt: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export type WorkerProfileResponse = {
|
export type WorkerProfileResponse = {
|
||||||
worker: WorkerUser
|
worker: WorkerUser
|
||||||
permissions: Record<string, unknown>
|
permissions: Record<string, unknown>
|
||||||
summary: WorkerProfileSummary
|
summary: WorkerProfileSummary
|
||||||
levelProgress: WorkerLevelProgress | null
|
levelProgress: WorkerLevelProgress | null
|
||||||
financeConfig: WorkerFinanceConfig
|
financeConfig: WorkerFinanceConfig
|
||||||
|
withdrawalAccounts: WorkerWithdrawalAccount[]
|
||||||
/** 打手端订单详情是否展示流转记录(后台配置开关,默认关闭) */
|
/** 打手端订单详情是否展示流转记录(后台配置开关,默认关闭) */
|
||||||
orderTimelineVisible?: boolean
|
orderTimelineVisible?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user