优化待解冻, 平台杂项配置优化
This commit is contained in:
@@ -31,3 +31,4 @@ apps/backend/data/logs
|
||||
send_code/
|
||||
.reasonix/
|
||||
*.tsbuildinfo
|
||||
.zcode/
|
||||
@@ -27,18 +27,29 @@ export async function sumPendingUnfreezeByOrderIds(
|
||||
export async function listDueDepositUnfreezes({
|
||||
limit = 100,
|
||||
workerId = 0,
|
||||
}: { limit?: number; workerId?: number } = {}): Promise<WorkerDepositUnfreezeRow[]> {
|
||||
retroactiveDays = -1,
|
||||
}: {
|
||||
limit?: number
|
||||
workerId?: number
|
||||
/** >=0 时按当前配置回溯:created_at + retroactiveDays 天也视为到期,配置调小后存量提前释放。 */
|
||||
retroactiveDays?: number
|
||||
}): Promise<WorkerDepositUnfreezeRow[]> {
|
||||
const params: unknown[] = []
|
||||
let workerClause = ''
|
||||
if (workerId > 0) {
|
||||
params.push(workerId)
|
||||
workerClause = `AND worker_id = $${params.length}`
|
||||
}
|
||||
let dueClause = 'unfreeze_at <= NOW()'
|
||||
if (Number.isFinite(retroactiveDays) && retroactiveDays >= 0) {
|
||||
params.push(Math.floor(retroactiveDays))
|
||||
dueClause = `(unfreeze_at <= NOW() OR created_at + ($${params.length} * INTERVAL '1 day') <= NOW())`
|
||||
}
|
||||
params.push(limit)
|
||||
const result = await query<WorkerDepositUnfreezeRow>(
|
||||
`
|
||||
SELECT * FROM worker_deposit_unfreezes
|
||||
WHERE status = 'pending' AND unfreeze_at <= NOW() ${workerClause}
|
||||
WHERE status = 'pending' AND ${dueClause} ${workerClause}
|
||||
ORDER BY unfreeze_at ASC
|
||||
LIMIT $${params.length}
|
||||
`,
|
||||
|
||||
@@ -217,9 +217,13 @@ export async function createWorkerWithdrawRequest(
|
||||
export async function settleDueDepositUnfreezes(
|
||||
options: { limit?: number; workerId?: number } = {},
|
||||
) {
|
||||
// 到期判定按当前解冻天数回溯:管理员调小财务配置后,存量待解冻按新天数提前视为到期,
|
||||
// 否则入队时的旧天数会让"扫描 0 笔"一直持续到原到期时间。
|
||||
const retroactiveDays = getWorkerFinanceConfig().depositUnfreezeDays
|
||||
const due = await listDueDepositUnfreezes({
|
||||
limit: Math.max(1, Number(options.limit || 100)),
|
||||
workerId: Number(options.workerId || 0),
|
||||
retroactiveDays,
|
||||
})
|
||||
let processedCount = 0
|
||||
for (const unfreeze of due) {
|
||||
|
||||
@@ -1,29 +1,191 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { App, Button, Card, Form, Input, Popconfirm, Space, Switch, Typography } from 'antd'
|
||||
import { ReloadOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Popconfirm,
|
||||
Space,
|
||||
Switch,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
addAdminMockCustomShop,
|
||||
deleteAdminMockCustomShop,
|
||||
fetchAdminMockCustomShops,
|
||||
fetchAdminScheduledJobsConfig,
|
||||
fetchAdminWorkerAnnouncement,
|
||||
renameAdminMockCustomShop,
|
||||
runAdminScheduledJob,
|
||||
saveAdminWorkerAnnouncement,
|
||||
saveAdminScheduledJobsConfig,
|
||||
} from '@/services/admin'
|
||||
import type { AdminScheduledJobItem, AdminScheduledJobRuntimeState } from '@/types/admin'
|
||||
import type { WorkerAnnouncementConfig } from '@/types/worker-platform'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
import HallConfigPanel from './HallConfigPanel'
|
||||
|
||||
function isDepositUnfreezeJob(job: AdminScheduledJobItem) {
|
||||
return job.type === 'deposit_unfreeze' || job.id === 'deposit-unfreeze'
|
||||
}
|
||||
|
||||
export default function AnnouncementPanel() {
|
||||
return (
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<AnnouncementCard />
|
||||
<DepositUnfreezeCard />
|
||||
<MockCustomShopsCard />
|
||||
<HallConfigPanel />
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
function DepositUnfreezeCard() {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [running, setRunning] = useState(false)
|
||||
const [draft, setDraft] = useState<AdminScheduledJobItem | null>(null)
|
||||
|
||||
const jobsQuery = useQuery({
|
||||
queryKey: ['admin-scheduled-jobs'],
|
||||
queryFn: fetchAdminScheduledJobsConfig,
|
||||
})
|
||||
const source = jobsQuery.data?.data
|
||||
const currentJob = source?.source.jobs.find(isDepositUnfreezeJob) || null
|
||||
const runtime: AdminScheduledJobRuntimeState | null =
|
||||
source?.runtime.find((item) => item.id === (draft?.id || currentJob?.id)) || null
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(currentJob ? { ...currentJob } : null)
|
||||
}, [currentJob])
|
||||
|
||||
function patchJob(patch: Partial<AdminScheduledJobItem>) {
|
||||
setDraft((prev) => (prev ? { ...prev, ...patch } : prev))
|
||||
}
|
||||
|
||||
async function saveJob() {
|
||||
if (!draft || !source) return
|
||||
setSaving(true)
|
||||
try {
|
||||
await saveAdminScheduledJobsConfig({
|
||||
...source.source,
|
||||
jobs: source.source.jobs.map((item) => (item.id === draft.id ? draft : item)),
|
||||
})
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-scheduled-jobs'] })
|
||||
message.success('押金解冻配置已保存')
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '保存押金解冻配置失败')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function runNow() {
|
||||
const jobId = draft?.id || currentJob?.id || ''
|
||||
if (!jobId) return
|
||||
setRunning(true)
|
||||
try {
|
||||
await runAdminScheduledJob(jobId)
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-scheduled-jobs'] })
|
||||
message.success('押金解冻任务已执行')
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '执行押金解冻失败')
|
||||
} finally {
|
||||
setRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title="押金到期解冻"
|
||||
bordered={false}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Button icon={<ReloadOutlined />} loading={running} onClick={() => void runNow()}>
|
||||
立即解冻
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
loading={saving}
|
||||
disabled={!draft}
|
||||
onClick={() => void saveJob()}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
|
||||
验收后的报酬与押金进入待解冻队列,到期自动转入打手可用余额;
|
||||
解冻天数与报酬延迟在「财务」面板配置,调小后存量待解冻也会按新天数提前到期。
|
||||
</Typography.Paragraph>
|
||||
{draft ? (
|
||||
<Space direction="vertical" size={12} style={{ width: '100%', maxWidth: 520 }}>
|
||||
<Space wrap>
|
||||
<Switch
|
||||
checked={draft.enabled !== false}
|
||||
onChange={(enabled) => patchJob({ enabled })}
|
||||
/>
|
||||
<Tag color={draft.enabled !== false ? 'green' : 'default'}>
|
||||
{draft.enabled !== false ? '已启用' : '已停用'}
|
||||
</Tag>
|
||||
</Space>
|
||||
<Space wrap>
|
||||
<Typography.Text type="secondary">执行间隔(秒)</Typography.Text>
|
||||
<InputNumber
|
||||
min={30}
|
||||
max={3600}
|
||||
value={draft.intervalSeconds}
|
||||
onChange={(value) => patchJob({ intervalSeconds: Number(value || 60) })}
|
||||
/>
|
||||
<Typography.Text type="secondary">每次解冻数量</Typography.Text>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={2000}
|
||||
value={Number(draft.config?.scanLimit ?? 100)}
|
||||
onChange={(value) =>
|
||||
patchJob({ config: { ...draft.config, scanLimit: Number(value || 100) } })
|
||||
}
|
||||
/>
|
||||
</Space>
|
||||
</Space>
|
||||
) : (
|
||||
<Typography.Text type="secondary">加载中…</Typography.Text>
|
||||
)}
|
||||
{runtime ? (
|
||||
<Alert
|
||||
style={{ marginTop: 16, maxWidth: 640 }}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
function AnnouncementCard() {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -727,64 +727,8 @@ function ScheduledJobCard({
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
// 押金到期解冻配置已迁至 接单平台 → 杂项配置,这里不再渲染。
|
||||
return null
|
||||
}
|
||||
const accounts = job.config?.accounts || []
|
||||
const defaultThreshold = Number(job.config?.assetThreshold ?? 500)
|
||||
@@ -1021,9 +965,6 @@ 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 || '定时任务'
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user