押金解冻天数可配置与待解冻押金扣减,超时订单保留记录
This commit is contained in:
@@ -84,6 +84,7 @@ import {
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { asRecord, formatMoney, formatSharingShareStatus, resolveSharingShareStatusColor } from './shared'
|
||||
type FinanceConfigFormValues = {
|
||||
depositUnfreezeDays?: number
|
||||
recharge?: {
|
||||
enabled?: boolean
|
||||
channelName?: string
|
||||
@@ -168,6 +169,7 @@ export default function FinancePanel() {
|
||||
setSavingConfig(true)
|
||||
try {
|
||||
await saveAdminWorkerFinanceConfig({
|
||||
depositUnfreezeDays: Number(values.depositUnfreezeDays ?? 3),
|
||||
recharge: {
|
||||
enabled: values.recharge?.enabled !== false,
|
||||
channelName: String(values.recharge?.channelName || '').trim(),
|
||||
@@ -370,6 +372,19 @@ export default function FinancePanel() {
|
||||
</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="开启提现申请"
|
||||
@@ -528,6 +543,7 @@ function mapFinanceConfigToFormValues(
|
||||
config?: WorkerFinanceConfig,
|
||||
): FinanceConfigFormValues {
|
||||
return {
|
||||
depositUnfreezeDays: Number(config?.depositUnfreezeDays ?? 3),
|
||||
recharge: {
|
||||
enabled: config?.recharge.enabled !== false,
|
||||
channelName: config?.recharge.channelName || '',
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
App,
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
@@ -42,6 +43,7 @@ import {
|
||||
acceptAdminWorkOrder,
|
||||
createAdminMockWorkOrder,
|
||||
creditAdminWorkerWallet,
|
||||
deductAdminWorkOrderPendingDeposit,
|
||||
deleteAdminWorkCategory,
|
||||
deleteAdminWorkOrder,
|
||||
deleteAdminWorkerLevel,
|
||||
@@ -121,9 +123,11 @@ export default function WorkOrdersPanel() {
|
||||
const [materialScreenshots, setMaterialScreenshots] = useState<UploadedFile[]>([])
|
||||
const [sharingOrder, setSharingOrder] = useState<WorkOrder | null>(null)
|
||||
const [editOrder, setEditOrder] = useState<WorkOrder | null>(null)
|
||||
const [deductOrder, setDeductOrder] = useState<WorkOrder | null>(null)
|
||||
const [problemForm] = Form.useForm()
|
||||
const [resolutionForm] = Form.useForm()
|
||||
const [materialForm] = Form.useForm()
|
||||
const [deductForm] = Form.useForm<{ amount?: number; note?: string }>()
|
||||
const [editForm] = Form.useForm<{
|
||||
productName?: string
|
||||
platformOrderId?: string
|
||||
@@ -329,6 +333,29 @@ export default function WorkOrdersPanel() {
|
||||
})
|
||||
}
|
||||
|
||||
function openDeductModal(row: WorkOrder) {
|
||||
setDeductOrder(row)
|
||||
deductForm.setFieldsValue({
|
||||
amount: Math.round(Number(row.pendingUnfreezeAmount || 0)) / 100,
|
||||
note: '',
|
||||
})
|
||||
}
|
||||
|
||||
async function submitDeduct(values: { amount?: number; note?: string }) {
|
||||
if (!deductOrder) return
|
||||
const succeeded = await runAction(
|
||||
() =>
|
||||
deductAdminWorkOrderPendingDeposit(deductOrder.workOrderId, {
|
||||
amount: Number(values.amount || 0),
|
||||
note: String(values.note || '').trim(),
|
||||
}),
|
||||
'待解冻押金已扣减',
|
||||
)
|
||||
if (!succeeded) return
|
||||
setDeductOrder(null)
|
||||
deductForm.resetFields()
|
||||
}
|
||||
|
||||
async function submitEdit(values: {
|
||||
productName?: string
|
||||
platformOrderId?: string
|
||||
@@ -573,6 +600,15 @@ export default function WorkOrdersPanel() {
|
||||
验收
|
||||
</Button>
|
||||
) : null}
|
||||
{row.status === 'accepted' && Number(row.pendingUnfreezeAmount || 0) > 0 ? (
|
||||
<Button
|
||||
icon={<WarningOutlined />}
|
||||
danger
|
||||
onClick={() => openDeductModal(row)}
|
||||
>
|
||||
扣押金
|
||||
</Button>
|
||||
) : null}
|
||||
{['in_progress', 'pending_acceptance'].includes(row.status) ? (
|
||||
<Button
|
||||
icon={<WarningOutlined />}
|
||||
@@ -948,6 +984,50 @@ export default function WorkOrdersPanel() {
|
||||
) : null}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={`扣减待解冻押金 · ${deductOrder?.workOrderNo || ''}`}
|
||||
open={Boolean(deductOrder)}
|
||||
onCancel={() => {
|
||||
setDeductOrder(null)
|
||||
deductForm.resetFields()
|
||||
}}
|
||||
onOk={() => deductForm.submit()}
|
||||
destroyOnHidden
|
||||
>
|
||||
{deductOrder ? (
|
||||
<Form form={deductForm} layout="vertical" onFinish={submitDeduct}>
|
||||
<Alert
|
||||
showIcon
|
||||
type="warning"
|
||||
message={`该工单当前待解冻押金 ${formatMoney(
|
||||
deductOrder.pendingUnfreezeAmount,
|
||||
)},扣减后剩余部分仍按原计划解冻。`}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<Form.Item
|
||||
label="扣减金额"
|
||||
name="amount"
|
||||
rules={[{ required: true, message: '请输入扣减金额' }]}
|
||||
>
|
||||
<InputNumber
|
||||
min={0.01}
|
||||
max={Math.round(Number(deductOrder.pendingUnfreezeAmount || 0)) / 100}
|
||||
step={1}
|
||||
addonAfter="元"
|
||||
className="full-width"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="扣减原因"
|
||||
name="note"
|
||||
rules={[{ required: true, message: '请填写扣减原因' }]}
|
||||
>
|
||||
<Input.TextArea rows={3} placeholder="如:订单出现问题,按规则扣除押金" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
) : null}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="标记问题单"
|
||||
open={Boolean(problemOrder)}
|
||||
|
||||
@@ -663,6 +663,72 @@ function ScheduledJobCard({
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (job.type === 'deposit_unfreeze' || job.id === 'deposit-unfreeze') {
|
||||
return (
|
||||
<Card
|
||||
size="small"
|
||||
title={
|
||||
<Space wrap>
|
||||
<Switch
|
||||
checked={job.enabled !== false}
|
||||
onChange={(enabled) => onChange({ ...job, enabled })}
|
||||
/>
|
||||
<span>{formatScheduledJobTitle(job)}</span>
|
||||
<Tag color={job.enabled ? 'green' : 'default'}>
|
||||
{job.enabled ? '已启用' : '已停用'}
|
||||
</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} loading={running} onClick={onRun}>
|
||||
立即解冻
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
|
||||
将有押金工单验收通过后进入待解冻的押金,在到期后自动转入可用余额(验收后 3
|
||||
天到账)。
|
||||
</Typography.Paragraph>
|
||||
<div className="platform-form-grid">
|
||||
<NumberField
|
||||
label="执行间隔(秒)"
|
||||
value={job.intervalSeconds}
|
||||
min={1}
|
||||
onChange={(intervalSeconds) => onChange({ ...job, intervalSeconds })}
|
||||
/>
|
||||
<NumberField
|
||||
label="每次解冻数量"
|
||||
value={Number(job.config?.scanLimit ?? 100)}
|
||||
min={1}
|
||||
onChange={(scanLimit) =>
|
||||
onChange({ ...job, config: { ...job.config, scanLimit } })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{runtime ? (
|
||||
<Alert
|
||||
className="platform-section-gap"
|
||||
type={
|
||||
runtime.lastStatus === 'ok' || runtime.lastStatus === 'success'
|
||||
? 'success'
|
||||
: runtime.lastStatus
|
||||
? 'warning'
|
||||
: 'info'
|
||||
}
|
||||
showIcon
|
||||
message={runtime.lastMessage || '尚未运行'}
|
||||
description={[
|
||||
`扫描 ${runtime.lastCheckedCount ?? 0}`,
|
||||
`解冻 ${runtime.lastAsset ?? 0}`,
|
||||
`上次:${formatAdminDateTime(runtime.lastFinishedAt || runtime.lastRunAt)}`,
|
||||
`下次:${formatAdminDateTime(runtime.nextRunAt)}`,
|
||||
].join(' · ')}
|
||||
/>
|
||||
) : null}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
const accounts = job.config?.accounts || []
|
||||
const defaultThreshold = Number(job.config?.assetThreshold ?? 500)
|
||||
|
||||
@@ -891,6 +957,9 @@ function formatScheduledJobTitle(job: AdminScheduledJobItem) {
|
||||
if (job.type === 'work_order_timeout' || job.id === 'work-order-timeout') {
|
||||
return '接单工单超时扫描'
|
||||
}
|
||||
if (job.type === 'deposit_unfreeze' || job.id === 'deposit-unfreeze') {
|
||||
return '押金到期解冻'
|
||||
}
|
||||
return job.id || job.type || '定时任务'
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import { formatDateTime } from '@/utils/date-time'
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: '', label: '全部订单' },
|
||||
{ value: 'open', label: '已超时' },
|
||||
{ value: 'in_progress', label: '代练中' },
|
||||
{ value: 'pending_acceptance', label: '待验收' },
|
||||
{ value: 'problem', label: '问题单' },
|
||||
@@ -52,6 +53,7 @@ const STATUS_SUMMARY_ITEMS = [
|
||||
{ key: 'in_progress', label: '代练中', note: '正在执行中的订单' },
|
||||
{ key: 'pending_acceptance', label: '待验收', note: '已提交,等待审核' },
|
||||
{ key: 'problem', label: '问题单', note: '需要补充或重新处理' },
|
||||
{ key: 'open', label: '已超时', note: '超时被系统退回的订单数' },
|
||||
{ key: 'accepted', label: '已验收', note: '已完成并结算的订单' },
|
||||
{ key: 'cancelled', label: '已取消', note: '已结束且不再继续' },
|
||||
] as const
|
||||
@@ -730,6 +732,7 @@ function formatMoney(value: number | undefined) {
|
||||
|
||||
function formatStatus(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
open: '已超时',
|
||||
in_progress: '代练中',
|
||||
pending_acceptance: '待验收',
|
||||
problem: '问题单',
|
||||
@@ -741,6 +744,7 @@ function formatStatus(status: string) {
|
||||
|
||||
function getStatusHint(status: string) {
|
||||
const hints: Record<string, string> = {
|
||||
open: '任务已超时被系统判定失败,如需继续可前往大厅重新抢单。',
|
||||
in_progress: '订单进行中,完成后请及时提交验收资料。',
|
||||
pending_acceptance: '已提交验收,等待后台审核。',
|
||||
problem: '后台已标记问题,请根据备注调整后重新提交。',
|
||||
@@ -755,6 +759,7 @@ function resolveStatusColor(status: string) {
|
||||
if (status === 'cancelled') return 'default'
|
||||
if (status === 'problem') return 'red'
|
||||
if (status === 'pending_acceptance') return 'gold'
|
||||
if (status === 'open') return 'red'
|
||||
return 'blue'
|
||||
}
|
||||
|
||||
|
||||
@@ -653,6 +653,8 @@ export default function WorkerProfilePage() {
|
||||
{ value: 'withdraw_paid', label: '提现打款' },
|
||||
{ value: 'deposit_freeze', label: '冻结押金' },
|
||||
{ value: 'deposit_release', label: '释放押金' },
|
||||
{ value: 'deposit_pending_unfreeze', label: '押金待解冻' },
|
||||
{ value: 'deposit_unfreeze', label: '押金已解冻' },
|
||||
{ value: 'deposit_deduction', label: '扣除押金' },
|
||||
{ value: 'reward_settlement', label: '结算报酬' },
|
||||
]}
|
||||
@@ -904,6 +906,14 @@ export default function WorkerProfilePage() {
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
) : null}
|
||||
<Alert
|
||||
showIcon
|
||||
type="warning"
|
||||
message={`每天限提现 1 次;有押金的工单验收通过后,押金需 ${Number(
|
||||
financeConfig?.depositUnfreezeDays ?? 3,
|
||||
)} 天解冻到账后方可提现。`}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<Form.Item label="可提现余额">
|
||||
<Typography.Text strong>
|
||||
{formatMoney(resolveAvailableForWithdraw(worker, summary))}
|
||||
@@ -1057,7 +1067,14 @@ function buildMetricCards(worker: WorkerUser, summary?: WorkerProfileSummary): M
|
||||
{
|
||||
label: '冻结押金',
|
||||
value: toYuan(worker.wallet.frozenDepositAmount),
|
||||
note: '抢单后冻结,验收通过会自动释放',
|
||||
note: '抢单后冻结,验收通过后 3 天自动解冻',
|
||||
prefix: '¥',
|
||||
precision: 2,
|
||||
},
|
||||
{
|
||||
label: '待解冻押金',
|
||||
value: toYuan(worker.wallet.pendingUnfreezeAmount),
|
||||
note: '验收通过后待 3 天解冻到账的押金',
|
||||
prefix: '¥',
|
||||
precision: 2,
|
||||
},
|
||||
@@ -1146,6 +1163,8 @@ function formatLedgerType(ledgerType: string) {
|
||||
withdraw_paid: '提现打款',
|
||||
deposit_freeze: '冻结押金',
|
||||
deposit_release: '释放押金',
|
||||
deposit_pending_unfreeze: '押金待解冻',
|
||||
deposit_unfreeze: '押金已解冻',
|
||||
deposit_deduction: '扣除押金',
|
||||
reward_settlement: '结算报酬',
|
||||
}
|
||||
@@ -1157,6 +1176,8 @@ function resolveLedgerTagColor(ledgerType: string) {
|
||||
if (ledgerType === 'withdraw_paid') return 'purple'
|
||||
if (ledgerType === 'deposit_freeze') return 'gold'
|
||||
if (ledgerType === 'deposit_release') return 'green'
|
||||
if (ledgerType === 'deposit_pending_unfreeze') return 'orange'
|
||||
if (ledgerType === 'deposit_unfreeze') return 'green'
|
||||
if (ledgerType === 'deposit_deduction') return 'red'
|
||||
if (ledgerType === 'reward_settlement') return 'cyan'
|
||||
return 'default'
|
||||
|
||||
Reference in New Issue
Block a user