684 lines
23 KiB
TypeScript
684 lines
23 KiB
TypeScript
import { ReloadOutlined } from '@ant-design/icons'
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import {
|
|
App,
|
|
Button,
|
|
Card,
|
|
Descriptions,
|
|
Form,
|
|
Input,
|
|
InputNumber,
|
|
Modal,
|
|
Select,
|
|
Space,
|
|
Switch,
|
|
Table,
|
|
Tag,
|
|
Typography,
|
|
} from 'antd'
|
|
import type { TableColumnsType } from 'antd'
|
|
import { useEffect, useState } from 'react'
|
|
|
|
import ImageUpload from '@/components/files/ImageUpload'
|
|
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
|
import {
|
|
fetchAdminWorkerFinanceConfig,
|
|
fetchAdminWorkerFinanceRequests,
|
|
reviewAdminWorkerFinanceRequest,
|
|
saveAdminWorkerFinanceConfig,
|
|
} from '@/services/admin'
|
|
import type {
|
|
UploadedFile,
|
|
WorkerFinanceConfig,
|
|
WorkerFinanceRequest,
|
|
} from '@/types/worker-platform'
|
|
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
|
import { asRecord, formatMoney } from './shared'
|
|
type FinanceConfigFormValues = {
|
|
depositUnfreezeDays?: number
|
|
recharge?: {
|
|
enabled?: boolean
|
|
channelName?: string
|
|
accountName?: string
|
|
accountNo?: string
|
|
qrCodeImageList?: UploadedFile[]
|
|
instructions?: string
|
|
}
|
|
adminContact?: {
|
|
wechatQrCodeImageList?: UploadedFile[]
|
|
}
|
|
withdraw?: {
|
|
enabled?: boolean
|
|
instructions?: string
|
|
}
|
|
}
|
|
|
|
type FinanceReviewAction = 'approved' | 'rejected' | 'cancelled'
|
|
|
|
export default function FinancePanel() {
|
|
const { message } = App.useApp()
|
|
const queryClient = useQueryClient()
|
|
const [configForm] = Form.useForm<FinanceConfigFormValues>()
|
|
const [reviewForm] = Form.useForm<{ reviewedNote?: string }>()
|
|
const [requestStatus, setRequestStatus] = useState('')
|
|
const [requestType, setRequestType] = useState('')
|
|
const [keywordInput, setKeywordInput] = useState('')
|
|
const [keyword, setKeyword] = useState('')
|
|
const [page, setPage] = useState(1)
|
|
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
|
const [savingConfig, setSavingConfig] = useState(false)
|
|
const [configExpanded, setConfigExpanded] = useState(false)
|
|
const [reviewing, setReviewing] = useState(false)
|
|
const [reviewState, setReviewState] = useState<{
|
|
action: FinanceReviewAction
|
|
request: WorkerFinanceRequest
|
|
} | null>(null)
|
|
|
|
const financeConfigQuery = useQuery({
|
|
queryKey: ['admin-worker-platform-finance-config'],
|
|
queryFn: () => fetchAdminWorkerFinanceConfig(),
|
|
})
|
|
const financeRequestsQuery = useQuery({
|
|
queryKey: [
|
|
'admin-worker-platform-finance-requests',
|
|
requestStatus,
|
|
requestType,
|
|
keyword,
|
|
page,
|
|
pageSize,
|
|
],
|
|
queryFn: () =>
|
|
fetchAdminWorkerFinanceRequests({
|
|
status: requestStatus,
|
|
requestType,
|
|
keyword,
|
|
page,
|
|
pageSize,
|
|
}),
|
|
})
|
|
const financeRequestsPagination = financeRequestsQuery.data?.data.pagination
|
|
|
|
useEffect(() => {
|
|
const config = financeConfigQuery.data?.data
|
|
if (!config) {
|
|
return
|
|
}
|
|
configForm.setFieldsValue(mapFinanceConfigToFormValues(config))
|
|
}, [financeConfigQuery.data, configForm])
|
|
|
|
async function refreshAll() {
|
|
await Promise.all([
|
|
queryClient.invalidateQueries({
|
|
queryKey: ['admin-worker-platform-finance-config'],
|
|
}),
|
|
queryClient.invalidateQueries({
|
|
queryKey: ['admin-worker-platform-finance-requests'],
|
|
}),
|
|
queryClient.invalidateQueries({
|
|
queryKey: ['admin-worker-platform-workers'],
|
|
}),
|
|
])
|
|
}
|
|
|
|
async function submitFinanceConfig(values: FinanceConfigFormValues) {
|
|
setSavingConfig(true)
|
|
try {
|
|
await saveAdminWorkerFinanceConfig({
|
|
depositUnfreezeDays: Number(values.depositUnfreezeDays ?? 3),
|
|
recharge: {
|
|
enabled: values.recharge?.enabled !== false,
|
|
channelName: String(values.recharge?.channelName || '').trim(),
|
|
accountName: String(values.recharge?.accountName || '').trim(),
|
|
accountNo: String(values.recharge?.accountNo || '').trim(),
|
|
qrCodeImage: values.recharge?.qrCodeImageList?.[0] || null,
|
|
instructions: String(values.recharge?.instructions || '').trim(),
|
|
},
|
|
adminContact: {
|
|
wechatQrCodeImage: values.adminContact?.wechatQrCodeImageList?.[0] || null,
|
|
},
|
|
withdraw: {
|
|
enabled: values.withdraw?.enabled !== false,
|
|
instructions: String(values.withdraw?.instructions || '').trim(),
|
|
},
|
|
})
|
|
message.success('资金配置已保存')
|
|
await refreshAll()
|
|
} catch (error) {
|
|
message.error(error instanceof Error ? error.message : '保存资金配置失败')
|
|
} finally {
|
|
setSavingConfig(false)
|
|
}
|
|
}
|
|
|
|
function openReviewModal(request: WorkerFinanceRequest, action: FinanceReviewAction) {
|
|
setReviewState({ request, action })
|
|
reviewForm.setFieldsValue({ reviewedNote: '' })
|
|
}
|
|
|
|
async function submitFinanceReview(values: { reviewedNote?: string }) {
|
|
if (!reviewState) return
|
|
setReviewing(true)
|
|
try {
|
|
await reviewAdminWorkerFinanceRequest(reviewState.request.requestId, {
|
|
status: reviewState.action,
|
|
reviewedNote: String(values.reviewedNote || '').trim(),
|
|
})
|
|
message.success(`${formatFinanceReviewAction(reviewState.action)}成功`)
|
|
setReviewState(null)
|
|
reviewForm.resetFields()
|
|
await refreshAll()
|
|
} catch (error) {
|
|
message.error(error instanceof Error ? error.message : '处理资金申请失败')
|
|
} finally {
|
|
setReviewing(false)
|
|
}
|
|
}
|
|
|
|
const requestColumns: TableColumnsType<WorkerFinanceRequest> = [
|
|
{
|
|
title: '打手',
|
|
minWidth: 180,
|
|
render: (_, row) => (
|
|
<div className="cell-stack">
|
|
<Typography.Text strong>
|
|
{row.worker?.displayName || row.worker?.username || '-'}
|
|
</Typography.Text>
|
|
<Typography.Text type="secondary">{row.worker?.username || '-'}</Typography.Text>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: '类型',
|
|
width: 120,
|
|
render: (_, row) => (
|
|
<Tag color={row.requestType === 'withdraw' ? 'green' : 'blue'}>
|
|
{formatFinanceRequestType(row.requestType)}
|
|
</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '金额',
|
|
width: 120,
|
|
render: (_, row) => formatMoney(row.amount),
|
|
},
|
|
{
|
|
title: '收款信息',
|
|
minWidth: 240,
|
|
render: (_, row) => renderFinanceRequestAccount(row),
|
|
},
|
|
{
|
|
title: '凭证/收款码',
|
|
minWidth: 140,
|
|
render: (_, row) => (
|
|
<ImagePreviewList
|
|
files={
|
|
row.requestType === 'recharge' ? getRechargeProofs(row) : getWechatWithdrawQrCode(row)
|
|
}
|
|
size={48}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
title: '备注',
|
|
minWidth: 260,
|
|
render: (_, row) => (
|
|
<div className="cell-stack">
|
|
<Typography.Text>{row.note || '-'}</Typography.Text>
|
|
{row.reviewedNote ? (
|
|
<Typography.Text type="secondary">审核备注:{row.reviewedNote}</Typography.Text>
|
|
) : null}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: '状态',
|
|
width: 120,
|
|
render: (_, row) => (
|
|
<Tag color={resolveFinanceRequestStatusColor(row.status)}>
|
|
{formatFinanceRequestStatus(row.status)}
|
|
</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '时间',
|
|
width: 180,
|
|
render: (_, row) => (
|
|
<div className="cell-stack">
|
|
<Typography.Text>{formatAdminDateTime(row.createdAt)}</Typography.Text>
|
|
<Typography.Text type="secondary">
|
|
{row.reviewedAt ? `审核:${formatAdminDateTime(row.reviewedAt)}` : '待审核'}
|
|
</Typography.Text>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
title: '操作',
|
|
width: 240,
|
|
render: (_, row) =>
|
|
row.status === 'pending' ? (
|
|
<Space wrap>
|
|
<Button type="primary" onClick={() => openReviewModal(row, 'approved')}>
|
|
通过
|
|
</Button>
|
|
<Button onClick={() => openReviewModal(row, 'rejected')}>拒绝</Button>
|
|
<Button danger onClick={() => openReviewModal(row, 'cancelled')}>
|
|
取消
|
|
</Button>
|
|
</Space>
|
|
) : (
|
|
<Typography.Text type="secondary">已处理</Typography.Text>
|
|
),
|
|
},
|
|
]
|
|
|
|
return (
|
|
<section className="platform-panel-stack">
|
|
<Card
|
|
title="资金配置"
|
|
bordered={false}
|
|
loading={financeConfigQuery.isLoading}
|
|
extra={
|
|
<Space>
|
|
<Button type="link" onClick={() => setConfigExpanded((expanded) => !expanded)}>
|
|
{configExpanded ? '收起配置' : '展开配置'}
|
|
</Button>
|
|
<Button
|
|
icon={<ReloadOutlined />}
|
|
loading={financeConfigQuery.isFetching}
|
|
onClick={() => financeConfigQuery.refetch()}
|
|
>
|
|
刷新
|
|
</Button>
|
|
</Space>
|
|
}
|
|
>
|
|
{configExpanded && financeConfigQuery.error ? (
|
|
<Typography.Text type="danger">
|
|
{financeConfigQuery.error instanceof Error
|
|
? financeConfigQuery.error.message
|
|
: '读取资金配置失败'}
|
|
</Typography.Text>
|
|
) : null}
|
|
{configExpanded ? (
|
|
<Form
|
|
form={configForm}
|
|
layout="vertical"
|
|
onFinish={submitFinanceConfig}
|
|
initialValues={mapFinanceConfigToFormValues()}
|
|
>
|
|
<Card title="充值入口" size="small" style={{ marginBottom: 16 }}>
|
|
<Form.Item
|
|
label="开启充值申请"
|
|
name={['recharge', 'enabled']}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item label="收款通道名称" name={['recharge', 'channelName']}>
|
|
<Input placeholder="如:支付宝扫码、微信收款" />
|
|
</Form.Item>
|
|
<Form.Item label="收款人" name={['recharge', 'accountName']}>
|
|
<Input placeholder="请输入收款人姓名" />
|
|
</Form.Item>
|
|
<Form.Item label="收款账号" name={['recharge', 'accountNo']}>
|
|
<Input placeholder="请输入支付宝账号、微信号或银行卡号" />
|
|
</Form.Item>
|
|
<Form.Item label="收款二维码" name={['recharge', 'qrCodeImageList']}>
|
|
<ImageUpload scene="worker-finance" scope="admin" maxCount={1} />
|
|
</Form.Item>
|
|
<Form.Item label="充值说明" name={['recharge', 'instructions']}>
|
|
<Input.TextArea
|
|
rows={4}
|
|
placeholder="例如:转账后请填写付款时间、付款尾号、截图说明。"
|
|
/>
|
|
</Form.Item>
|
|
</Card>
|
|
|
|
<Card title="管理员联系" size="small" style={{ marginBottom: 16 }}>
|
|
<Form.Item label="管理员微信二维码" name={['adminContact', 'wechatQrCodeImageList']}>
|
|
<ImageUpload scene="worker-admin-contact" scope="admin" maxCount={1} />
|
|
</Form.Item>
|
|
</Card>
|
|
|
|
<Card title="押金与提现规则" size="small" style={{ marginBottom: 16 }}>
|
|
<Form.Item
|
|
label="押金解冻天数"
|
|
name="depositUnfreezeDays"
|
|
tooltip="有押金的工单验收通过后,押金进入待解冻,N 天后自动转入可用余额;0 表示验收通过立即释放"
|
|
>
|
|
<InputNumber min={0} max={30} step={1} addonAfter="天" />
|
|
</Form.Item>
|
|
<Typography.Text type="secondary">
|
|
解冻期间如订单出现问题,可在"接单工单"中对已验收工单扣减待解冻押金(全额或部分)。
|
|
</Typography.Text>
|
|
</Card>
|
|
|
|
<Card title="提现入口" size="small" style={{ marginBottom: 16 }}>
|
|
<Form.Item
|
|
label="开启提现申请"
|
|
name={['withdraw', 'enabled']}
|
|
valuePropName="checked"
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
<Form.Item label="提现说明" name={['withdraw', 'instructions']}>
|
|
<Input.TextArea
|
|
rows={4}
|
|
placeholder="例如:每天 18:00 前审核,当日打款;银行卡请备注开户行。"
|
|
/>
|
|
</Form.Item>
|
|
</Card>
|
|
|
|
<Button type="primary" htmlType="submit" loading={savingConfig}>
|
|
保存配置
|
|
</Button>
|
|
</Form>
|
|
) : null}
|
|
</Card>
|
|
|
|
<Card
|
|
title="资金申请"
|
|
bordered={false}
|
|
extra={
|
|
<Space wrap>
|
|
<Input.Search
|
|
allowClear
|
|
value={keywordInput}
|
|
placeholder="搜索打手账号/昵称/手机号"
|
|
style={{ width: 240 }}
|
|
onChange={(event) => setKeywordInput(event.target.value)}
|
|
onSearch={(value) => {
|
|
setKeyword(String(value || '').trim())
|
|
setPage(1)
|
|
}}
|
|
/>
|
|
<Select
|
|
value={requestType}
|
|
style={{ width: 140 }}
|
|
options={[
|
|
{ value: '', label: '全部类型' },
|
|
{ value: 'recharge', label: '充值申请' },
|
|
{ value: 'withdraw', label: '提现申请' },
|
|
]}
|
|
onChange={(nextType) => {
|
|
setRequestType(nextType)
|
|
setPage(1)
|
|
}}
|
|
/>
|
|
<Select
|
|
value={requestStatus}
|
|
style={{ width: 140 }}
|
|
options={[
|
|
{ value: '', label: '全部状态' },
|
|
{ value: 'pending', label: '待处理' },
|
|
{ value: 'approved', label: '已通过' },
|
|
{ value: 'rejected', label: '已拒绝' },
|
|
{ value: 'cancelled', label: '已取消' },
|
|
]}
|
|
onChange={(nextStatus) => {
|
|
setRequestStatus(nextStatus)
|
|
setPage(1)
|
|
}}
|
|
/>
|
|
<Button
|
|
icon={<ReloadOutlined />}
|
|
loading={financeRequestsQuery.isFetching}
|
|
onClick={() => financeRequestsQuery.refetch()}
|
|
>
|
|
刷新
|
|
</Button>
|
|
</Space>
|
|
}
|
|
>
|
|
<Table<WorkerFinanceRequest>
|
|
rowKey="requestId"
|
|
loading={financeRequestsQuery.isLoading}
|
|
dataSource={financeRequestsQuery.data?.data.items || []}
|
|
columns={requestColumns}
|
|
pagination={buildAdminTablePagination({
|
|
current: financeRequestsPagination?.page || page,
|
|
pageSize: financeRequestsPagination?.pageSize || pageSize,
|
|
total: financeRequestsPagination?.total || 0,
|
|
onChange: (nextPage, nextPageSize) => {
|
|
setPage(nextPage)
|
|
setPageSize(nextPageSize)
|
|
},
|
|
})}
|
|
scroll={{ x: 1380 }}
|
|
/>
|
|
</Card>
|
|
|
|
<Modal
|
|
title={
|
|
reviewState ? `${formatFinanceReviewAction(reviewState.action)}资金申请` : '处理资金申请'
|
|
}
|
|
open={Boolean(reviewState)}
|
|
destroyOnHidden
|
|
confirmLoading={reviewing}
|
|
onCancel={() => {
|
|
setReviewState(null)
|
|
reviewForm.resetFields()
|
|
}}
|
|
onOk={() => reviewForm.submit()}
|
|
>
|
|
{reviewState ? (
|
|
<Space direction="vertical" size={16} className="full-width">
|
|
<Descriptions column={1} bordered size="small">
|
|
<Descriptions.Item label="打手">
|
|
{reviewState.request.worker?.displayName ||
|
|
reviewState.request.worker?.username ||
|
|
'-'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="申请类型">
|
|
{formatFinanceRequestType(reviewState.request.requestType)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="金额">
|
|
{formatMoney(reviewState.request.amount)}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="收款信息">
|
|
{renderFinanceRequestAccountText(reviewState.request)}
|
|
</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' ? (
|
|
<>
|
|
<Descriptions.Item label="付款时间">
|
|
{getRechargePaidAt(reviewState.request)
|
|
? formatAdminDateTime(getRechargePaidAt(reviewState.request))
|
|
: '-'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="付款凭证">
|
|
<ImagePreviewList files={getRechargeProofs(reviewState.request)} size={88} />
|
|
</Descriptions.Item>
|
|
</>
|
|
) : 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="申请备注">
|
|
{reviewState.request.note || '-'}
|
|
</Descriptions.Item>
|
|
</Descriptions>
|
|
|
|
<Form form={reviewForm} layout="vertical" onFinish={submitFinanceReview}>
|
|
<Form.Item label="审核备注" name="reviewedNote">
|
|
<Input.TextArea
|
|
rows={4}
|
|
placeholder={`请输入${formatFinanceReviewAction(reviewState.action)}备注`}
|
|
/>
|
|
</Form.Item>
|
|
</Form>
|
|
</Space>
|
|
) : null}
|
|
</Modal>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function mapFinanceConfigToFormValues(config?: WorkerFinanceConfig): FinanceConfigFormValues {
|
|
return {
|
|
depositUnfreezeDays: Number(config?.depositUnfreezeDays ?? 3),
|
|
recharge: {
|
|
enabled: config?.recharge.enabled !== false,
|
|
channelName: config?.recharge.channelName || '',
|
|
accountName: config?.recharge.accountName || '',
|
|
accountNo: config?.recharge.accountNo || '',
|
|
qrCodeImageList: config?.recharge.qrCodeImage ? [config.recharge.qrCodeImage] : [],
|
|
instructions: config?.recharge.instructions || '',
|
|
},
|
|
adminContact: {
|
|
wechatQrCodeImageList: config?.adminContact.wechatQrCodeImage
|
|
? [config.adminContact.wechatQrCodeImage]
|
|
: [],
|
|
},
|
|
withdraw: {
|
|
enabled: config?.withdraw.enabled !== false,
|
|
instructions: config?.withdraw.instructions || '',
|
|
},
|
|
}
|
|
}
|
|
|
|
function formatFinanceReviewAction(action: FinanceReviewAction) {
|
|
if (action === 'approved') return '通过'
|
|
if (action === 'rejected') return '拒绝'
|
|
return '取消'
|
|
}
|
|
|
|
function formatFinanceRequestType(requestType: string) {
|
|
if (requestType === 'recharge') return '充值申请'
|
|
if (requestType === 'withdraw') return '提现申请'
|
|
return requestType || '-'
|
|
}
|
|
|
|
function formatFinanceRequestStatus(status: string) {
|
|
const labels: Record<string, string> = {
|
|
pending: '待处理',
|
|
approved: '已通过',
|
|
rejected: '已拒绝',
|
|
cancelled: '已取消',
|
|
}
|
|
return labels[status] || status || '-'
|
|
}
|
|
|
|
function resolveFinanceRequestStatusColor(status: string) {
|
|
if (status === 'approved') return 'green'
|
|
if (status === 'pending') return 'gold'
|
|
if (status === 'rejected') return 'red'
|
|
if (status === 'cancelled') return 'default'
|
|
return 'blue'
|
|
}
|
|
|
|
function renderFinanceRequestAccount(request: WorkerFinanceRequest) {
|
|
return (
|
|
<div className="cell-stack">
|
|
<Typography.Text>{renderFinanceRequestAccountText(request)}</Typography.Text>
|
|
{request.requestType === 'withdraw' && request.accountNo ? (
|
|
<Typography.Text copyable>{request.accountNo}</Typography.Text>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function renderFinanceRequestAccountText(request: WorkerFinanceRequest) {
|
|
if (request.requestType === 'withdraw') {
|
|
const channel = formatFinanceChannel(request.accountChannel)
|
|
const accountName = request.accountName || '-'
|
|
return `${channel} / ${accountName}`
|
|
}
|
|
|
|
const rechargeSnapshot = getRechargeSnapshot(request)
|
|
if (rechargeSnapshot.channelName || rechargeSnapshot.accountName || rechargeSnapshot.accountNo) {
|
|
return [
|
|
rechargeSnapshot.channelName,
|
|
rechargeSnapshot.accountName,
|
|
rechargeSnapshot.accountNo ? maskFinanceAccount(rechargeSnapshot.accountNo) : '',
|
|
]
|
|
.filter(Boolean)
|
|
.join(' / ')
|
|
}
|
|
|
|
return '线下充值,等待管理员核对'
|
|
}
|
|
|
|
function getRechargeSnapshot(request: WorkerFinanceRequest) {
|
|
const payload = asRecord(request.payload)
|
|
const financeConfig = asRecord(payload.financeConfig)
|
|
return {
|
|
channelName: String(financeConfig.channelName || '').trim(),
|
|
accountName: String(financeConfig.accountName || '').trim(),
|
|
accountNo: String(financeConfig.accountNo || '').trim(),
|
|
}
|
|
}
|
|
|
|
function getRechargeProofs(request: WorkerFinanceRequest): UploadedFile[] {
|
|
if (request.requestType !== 'recharge') return []
|
|
const payload = asRecord(request.payload)
|
|
const rawFiles = Array.isArray(payload.proofFiles) ? payload.proofFiles : []
|
|
return rawFiles
|
|
.map((item) => {
|
|
const file = asRecord(item)
|
|
const url = String(file.url || '').trim()
|
|
if (!url) return null
|
|
return {
|
|
url,
|
|
mediumUrl: String(file.mediumUrl || '').trim(),
|
|
thumbnailUrl: String(file.thumbnailUrl || '').trim(),
|
|
} as UploadedFile
|
|
})
|
|
.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 {
|
|
const payload = asRecord(request.payload)
|
|
return String(payload.paidAt || '').trim()
|
|
}
|
|
|
|
function formatFinanceChannel(channel: string) {
|
|
if (channel === 'alipay') return '支付宝'
|
|
if (channel === 'wechat') return '微信收款'
|
|
if (channel === 'bank') return '银行卡'
|
|
if (channel === 'manual') return '线下转账'
|
|
return channel || '-'
|
|
}
|
|
|
|
function maskFinanceAccount(value: string) {
|
|
const text = String(value || '').trim()
|
|
if (!text) return '-'
|
|
if (text.length <= 8) return text
|
|
return `${text.slice(0, 4)} **** ${text.slice(-4)}`
|
|
}
|