优化待解冻, 平台杂项配置优化
This commit is contained in:
@@ -31,3 +31,4 @@ apps/backend/data/logs
|
|||||||
send_code/
|
send_code/
|
||||||
.reasonix/
|
.reasonix/
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
.zcode/
|
||||||
@@ -27,18 +27,29 @@ export async function sumPendingUnfreezeByOrderIds(
|
|||||||
export async function listDueDepositUnfreezes({
|
export async function listDueDepositUnfreezes({
|
||||||
limit = 100,
|
limit = 100,
|
||||||
workerId = 0,
|
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[] = []
|
const params: unknown[] = []
|
||||||
let workerClause = ''
|
let workerClause = ''
|
||||||
if (workerId > 0) {
|
if (workerId > 0) {
|
||||||
params.push(workerId)
|
params.push(workerId)
|
||||||
workerClause = `AND worker_id = $${params.length}`
|
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)
|
params.push(limit)
|
||||||
const result = await query<WorkerDepositUnfreezeRow>(
|
const result = await query<WorkerDepositUnfreezeRow>(
|
||||||
`
|
`
|
||||||
SELECT * FROM worker_deposit_unfreezes
|
SELECT * FROM worker_deposit_unfreezes
|
||||||
WHERE status = 'pending' AND unfreeze_at <= NOW() ${workerClause}
|
WHERE status = 'pending' AND ${dueClause} ${workerClause}
|
||||||
ORDER BY unfreeze_at ASC
|
ORDER BY unfreeze_at ASC
|
||||||
LIMIT $${params.length}
|
LIMIT $${params.length}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -217,9 +217,13 @@ export async function createWorkerWithdrawRequest(
|
|||||||
export async function settleDueDepositUnfreezes(
|
export async function settleDueDepositUnfreezes(
|
||||||
options: { limit?: number; workerId?: number } = {},
|
options: { limit?: number; workerId?: number } = {},
|
||||||
) {
|
) {
|
||||||
|
// 到期判定按当前解冻天数回溯:管理员调小财务配置后,存量待解冻按新天数提前视为到期,
|
||||||
|
// 否则入队时的旧天数会让"扫描 0 笔"一直持续到原到期时间。
|
||||||
|
const retroactiveDays = getWorkerFinanceConfig().depositUnfreezeDays
|
||||||
const due = await listDueDepositUnfreezes({
|
const due = await listDueDepositUnfreezes({
|
||||||
limit: Math.max(1, Number(options.limit || 100)),
|
limit: Math.max(1, Number(options.limit || 100)),
|
||||||
workerId: Number(options.workerId || 0),
|
workerId: Number(options.workerId || 0),
|
||||||
|
retroactiveDays,
|
||||||
})
|
})
|
||||||
let processedCount = 0
|
let processedCount = 0
|
||||||
for (const unfreeze of due) {
|
for (const unfreeze of due) {
|
||||||
|
|||||||
@@ -1,29 +1,191 @@
|
|||||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
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 { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addAdminMockCustomShop,
|
addAdminMockCustomShop,
|
||||||
deleteAdminMockCustomShop,
|
deleteAdminMockCustomShop,
|
||||||
fetchAdminMockCustomShops,
|
fetchAdminMockCustomShops,
|
||||||
|
fetchAdminScheduledJobsConfig,
|
||||||
fetchAdminWorkerAnnouncement,
|
fetchAdminWorkerAnnouncement,
|
||||||
renameAdminMockCustomShop,
|
renameAdminMockCustomShop,
|
||||||
|
runAdminScheduledJob,
|
||||||
saveAdminWorkerAnnouncement,
|
saveAdminWorkerAnnouncement,
|
||||||
|
saveAdminScheduledJobsConfig,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
|
import type { AdminScheduledJobItem, AdminScheduledJobRuntimeState } from '@/types/admin'
|
||||||
import type { WorkerAnnouncementConfig } from '@/types/worker-platform'
|
import type { WorkerAnnouncementConfig } from '@/types/worker-platform'
|
||||||
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
|
|
||||||
import HallConfigPanel from './HallConfigPanel'
|
import HallConfigPanel from './HallConfigPanel'
|
||||||
|
|
||||||
|
function isDepositUnfreezeJob(job: AdminScheduledJobItem) {
|
||||||
|
return job.type === 'deposit_unfreeze' || job.id === 'deposit-unfreeze'
|
||||||
|
}
|
||||||
|
|
||||||
export default function AnnouncementPanel() {
|
export default function AnnouncementPanel() {
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||||
<AnnouncementCard />
|
<AnnouncementCard />
|
||||||
|
<DepositUnfreezeCard />
|
||||||
<MockCustomShopsCard />
|
<MockCustomShopsCard />
|
||||||
<HallConfigPanel />
|
<HallConfigPanel />
|
||||||
</Space>
|
</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() {
|
function AnnouncementCard() {
|
||||||
const { message } = App.useApp()
|
const { message } = App.useApp()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|||||||
@@ -727,64 +727,8 @@ function ScheduledJobCard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (job.type === 'deposit_unfreeze' || job.id === 'deposit-unfreeze') {
|
if (job.type === 'deposit_unfreeze' || job.id === 'deposit-unfreeze') {
|
||||||
return (
|
// 押金到期解冻配置已迁至 接单平台 → 杂项配置,这里不再渲染。
|
||||||
<Card
|
return null
|
||||||
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 accounts = job.config?.accounts || []
|
||||||
const defaultThreshold = Number(job.config?.assetThreshold ?? 500)
|
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') {
|
if (job.type === 'work_order_timeout' || job.id === 'work-order-timeout') {
|
||||||
return '接单工单超时扫描'
|
return '接单工单超时扫描'
|
||||||
}
|
}
|
||||||
if (job.type === 'deposit_unfreeze' || job.id === 'deposit-unfreeze') {
|
|
||||||
return '押金到期解冻'
|
|
||||||
}
|
|
||||||
return job.id || job.type || '定时任务'
|
return job.id || job.type || '定时任务'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user