优化待解冻, 平台杂项配置优化

This commit is contained in:
yml2213
2026-08-22 20:11:01 +08:00
parent 1b8fdff162
commit b4918769f1
5 changed files with 183 additions and 64 deletions
+1
View File
@@ -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 || '定时任务'
}