押金解冻天数可配置与待解冻押金扣减,超时订单保留记录

This commit is contained in:
yml2213
2026-08-02 10:14:05 +08:00
parent 043dd3c128
commit 93df3d1c9b
20 changed files with 854 additions and 38 deletions
@@ -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'
@@ -140,6 +140,7 @@ export function fetchAdminWorkerFinanceConfig() {
}
export function saveAdminWorkerFinanceConfig(payload: {
depositUnfreezeDays?: number
recharge?: {
enabled?: boolean
channelName?: string
@@ -298,3 +299,13 @@ export function acceptAdminWorkOrder(workOrderId: number) {
`/api/v1/admin/worker-platform/orders/${workOrderId}/accept`,
)
}
export function deductAdminWorkOrderPendingDeposit(
workOrderId: number,
payload: { amount: number; note: string },
) {
return apiPost<{ deductedAmount: number }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/deduct-pending-deposit`,
payload,
)
}
@@ -25,6 +25,7 @@ export type WorkerUser = {
wallet: {
availableAmount: number
frozenDepositAmount: number
pendingUnfreezeAmount: number
totalCreditedAmount: number
totalSettledAmount: number
}
@@ -84,6 +85,7 @@ export type WorkerFinanceRequest = {
}
export type WorkerFinanceConfig = {
depositUnfreezeDays: number
recharge: {
enabled: boolean
channelName: string
@@ -225,6 +227,7 @@ export type WorkOrder = {
joinedQuantity: number
pendingSubmissionCount: number
}
pendingUnfreezeAmount?: number
timeoutMinutes?: number
timeoutPolicy?: 'reopen' | 'cancel_release' | 'cancel_deduct' | string
deadlineAt?: string | null