新增数据迁移任务到定时任务白名单;优化默认收货时间范围逻辑

This commit is contained in:
wuweihua
2026-11-11 15:34:11 +08:00
parent f82d9c667f
commit 7869205afc
14 changed files with 1251 additions and 3 deletions
+55
View File
@@ -0,0 +1,55 @@
import request from '@/utils/request'
export function listMigration(query) {
return request({
url: '/tool/migration/list',
method: 'get',
params: query
})
}
export function getMigration(migrationId) {
return request({
url: '/tool/migration/' + migrationId,
method: 'get'
})
}
export function addMigration(data) {
return request({
url: '/tool/migration',
method: 'post',
data
})
}
export function updateMigration(data) {
return request({
url: '/tool/migration',
method: 'put',
data
})
}
export function delMigration(migrationIds) {
return request({
url: '/tool/migration/' + migrationIds,
method: 'delete'
})
}
export function runMigration(migrationId) {
return request({
url: '/tool/migration/run/' + migrationId,
method: 'post',
timeout: 300000
})
}
export function listMigrationLog(query) {
return request({
url: '/tool/migration/log/list',
method: 'get',
params: query
})
}
+21 -1
View File
@@ -436,9 +436,29 @@ const exportFieldRef = ref()
const tableHeight = ref(480)
let tableResizeObserver = null
function pad2(n) {
return String(n).padStart(2, '0')
}
function formatDateTime(d, endOfDay) {
const y = d.getFullYear()
const m = pad2(d.getMonth() + 1)
const day = pad2(d.getDate())
return endOfDay ? `${y}-${m}-${day} 23:59:59` : `${y}-${m}-${day} 00:00:00`
}
/** 默认收货时间:最近两个星期(含今天共 14 天) */
function defaultPickupDateRange() {
const end = new Date()
const start = new Date()
start.setDate(start.getDate() - 13)
return [formatDateTime(start, false), formatDateTime(end, true)]
}
const defaultParams = {
orderStatuses: [3, 4, 5, 6, 7, 8, 11],
billingFailStatuses: []
billingFailStatuses: [],
pickupDateRange: defaultPickupDateRange()
}
const orderStatusMap = {
@@ -0,0 +1,378 @@
<template>
<div class="app-container">
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
row-key="migrationId"
selection
>
<template #toolbar="{ selection, ids }">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" v-hasPermi="['tool:migration:add']" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="selection.length !== 1" v-hasPermi="['tool:migration:edit']" @click="handleUpdate(selection[0])">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="!selection.length" v-hasPermi="['tool:migration:remove']" @click="handleDelete(ids)">删除</el-button>
</el-col>
</template>
<template #col-status="{ row }">
<el-tag size="small" :type="row.status === '0' ? 'success' : 'info'">{{ row.status === '0' ? '启用调度' : '停用' }}</el-tag>
</template>
<template #col-lastRunStatus="{ row }">
<el-tag v-if="row.lastRunStatus === '0'" size="small" type="success">成功</el-tag>
<el-tag v-else-if="row.lastRunStatus === '1'" size="small" type="danger">失败</el-tag>
<span v-else>-</span>
</template>
<template #col-cronExpression="{ row }">
<span>{{ scheduleLabel(row.cronExpression) }}</span>
</template>
<template #col-action="{ row }">
<el-button link type="primary" v-hasPermi="['tool:migration:run']" @click="handleRun(row)">立即执行</el-button>
<el-button link type="primary" v-hasPermi="['tool:migration:log']" @click="openLog(row)">日志</el-button>
<el-button link type="primary" v-hasPermi="['tool:migration:edit']" @click="handleUpdate(row)">修改</el-button>
<el-button link type="primary" v-hasPermi="['tool:migration:remove']" @click="handleDelete(row.migrationId)">删除</el-button>
</template>
</basic-table>
<el-dialog :title="title" v-model="open" width="780px" append-to-body destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="脚本名称" prop="migrationName">
<el-input v-model="form.migrationName" maxlength="100" placeholder="请输入脚本名称" />
</el-form-item>
<el-form-item label="SQL" prop="sqlText">
<el-input v-model="form.sqlText" type="textarea" :rows="8" placeholder="支持多条语句,以分号分隔;禁止危险语句" />
</el-form-item>
<el-form-item label="执行计划">
<el-radio-group v-model="schedule.scheduleType" @change="onScheduleTypeChange">
<el-radio-button value="none">不调度</el-radio-button>
<el-radio-button value="once">指定时间</el-radio-button>
<el-radio-button value="daily">每天</el-radio-button>
<el-radio-button value="weekly">每周</el-radio-button>
<el-radio-button value="monthly">每月</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="schedule.scheduleType === 'once'" label="执行时间" required>
<el-date-picker v-model="schedule.runAt" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm" placeholder="选择日期时间" style="width: 100%" />
</el-form-item>
<el-form-item v-if="schedule.scheduleType === 'weekly'" label="星期" required>
<el-checkbox-group v-model="schedule.weekDays">
<el-checkbox v-for="d in weekOptions" :key="d.value" :value="d.value">{{ d.label }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item v-if="schedule.scheduleType === 'monthly'" label="日期" required>
<el-select v-model="schedule.monthDay" placeholder="每月几号" style="width: 180px">
<el-option v-for="d in monthDayOptions" :key="d" :label="d + '号'" :value="d" />
</el-select>
</el-form-item>
<el-form-item v-if="['daily','weekly','monthly'].includes(schedule.scheduleType)" label="时间" required>
<el-time-picker v-model="schedule.runTime" value-format="HH:mm" format="HH:mm" placeholder="选择时间" style="width: 180px" />
</el-form-item>
<el-form-item v-if="schedule.scheduleType !== 'none'" label="Cron预览">
<el-input :model-value="previewCron" readonly>
<template #append>{{ scheduleLabel(previewCron) }}</template>
</el-input>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio value="0">启用调度</el-radio>
<el-radio value="1">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" />
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" :loading="saving" @click="submitForm">保存</el-button>
<el-button @click="open = false">取消</el-button>
</template>
</el-dialog>
<el-drawer v-model="logOpen" title="执行日志" size="60%" destroy-on-close>
<el-table v-loading="logLoading" :data="logRows" border height="calc(100vh - 160px)">
<el-table-column prop="triggerType" label="触发方式" width="100" align="center">
<template #default="{ row }">{{ row.triggerType === 'CRON' ? '定时' : '手动' }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag size="small" :type="row.status === '0' ? 'success' : 'danger'">{{ row.status === '0' ? '成功' : '失败' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="affectedRows" label="影响行数" width="100" align="center" />
<el-table-column prop="startTime" label="开始时间" width="170" />
<el-table-column prop="endTime" label="结束时间" width="170" />
<el-table-column prop="message" label="信息" min-width="180" show-overflow-tooltip />
</el-table>
<template #footer>
<el-button @click="logOpen = false">关闭</el-button>
</template>
</el-drawer>
</div>
</template>
<script setup name="DataMigration">
import {
listMigration, getMigration, addMigration, updateMigration, delMigration, runMigration, listMigrationLog
} from '@/api/tool/migration'
const { proxy } = getCurrentInstance()
const tableRef = ref()
const formRef = ref()
const open = ref(false)
const title = ref('')
const saving = ref(false)
const logOpen = ref(false)
const logLoading = ref(false)
const logRows = ref([])
const currentMigrationId = ref(null)
const weekOptions = [
{ label: '周一', value: '2' },
{ label: '周二', value: '3' },
{ label: '周三', value: '4' },
{ label: '周四', value: '5' },
{ label: '周五', value: '6' },
{ label: '周六', value: '7' },
{ label: '周日', value: '1' }
]
const weekLabelMap = Object.fromEntries(weekOptions.map(i => [i.value, i.label]))
const monthDayOptions = Array.from({ length: 31 }, (_, i) => i + 1)
const form = ref({})
const schedule = reactive({
scheduleType: 'none',
runAt: '',
runTime: '02:00',
weekDays: ['2'],
monthDay: 1
})
const rules = {
migrationName: [{ required: true, message: '脚本名称不能为空', trigger: 'blur' }],
sqlText: [{ required: true, message: 'SQL 不能为空', trigger: 'blur' }]
}
const searchColumns = [
{ label: '脚本名称', prop: 'migrationName', placeholder: '脚本名称' },
{
label: '状态',
prop: 'status',
component: 'select',
options: [
{ label: '启用调度', value: '0' },
{ label: '停用', value: '1' }
],
props: { clearable: true }
}
]
const columns = [
{ label: '脚本名称', prop: 'migrationName', minWidth: 160, showOverflowTooltip: true },
{ label: '执行计划', prop: 'cronExpression', minWidth: 180, showOverflowTooltip: true, slot: 'cronExpression' },
{ label: '状态', prop: 'status', width: 110, align: 'center', slot: 'status' },
{ label: '最近状态', prop: 'lastRunStatus', width: 100, align: 'center', slot: 'lastRunStatus' },
{ label: '最近执行', prop: 'lastRunTime', width: 170, parseTime: true },
{ label: '备注', prop: 'remark', minWidth: 140, showOverflowTooltip: true },
{ label: '操作', prop: 'action', width: 260, align: 'center', fixed: 'right', slot: 'action' }
]
const previewCron = computed(() => buildCronFromSchedule(schedule) || '')
function pad2(n) {
return String(n).padStart(2, '0')
}
function parseHm(hm) {
const parts = String(hm || '02:00').split(':')
const h = Number(parts[0] || 0)
const m = Number(parts[1] || 0)
return { h: Number.isFinite(h) ? h : 0, m: Number.isFinite(m) ? m : 0 }
}
function buildCronFromSchedule(s) {
if (!s || s.scheduleType === 'none') return ''
if (s.scheduleType === 'once') {
if (!s.runAt) return ''
const d = new Date(String(s.runAt).replace('T', ' '))
if (Number.isNaN(d.getTime())) return ''
return '0 ' + d.getMinutes() + ' ' + d.getHours() + ' ' + d.getDate() + ' ' + (d.getMonth() + 1) + ' ? ' + d.getFullYear()
}
const { h, m } = parseHm(s.runTime)
if (s.scheduleType === 'daily') return '0 ' + m + ' ' + h + ' * * ?'
if (s.scheduleType === 'weekly') {
const days = (s.weekDays || []).slice().sort()
if (!days.length) return ''
return '0 ' + m + ' ' + h + ' ? * ' + days.join(',')
}
if (s.scheduleType === 'monthly') {
const day = Number(s.monthDay || 1)
return '0 ' + m + ' ' + h + ' ' + day + ' * ?'
}
return ''
}
function parseCronToSchedule(cron) {
const empty = {
scheduleType: 'none',
runAt: '',
runTime: '02:00',
weekDays: ['2'],
monthDay: 1
}
const text = String(cron || '').trim()
if (!text) return empty
const parts = text.split(/\s+/)
if (parts.length < 6) return empty
const min = parts[1]
const hour = parts[2]
const day = parts[3]
const month = parts[4]
const week = parts[5]
const year = parts[6]
const runTime = pad2(hour) + ':' + pad2(min)
if (year && /^\d{4}$/.test(year) && day !== '*' && day !== '?' && month !== '*' && week === '?') {
return { ...empty, scheduleType: 'once', runAt: year + '-' + pad2(month) + '-' + pad2(day) + ' ' + runTime + ':00', runTime }
}
if (day === '*' && month === '*' && (week === '?' || week === '*')) {
return { ...empty, scheduleType: 'daily', runTime }
}
if (day === '?' && month === '*' && week && week !== '?' && week !== '*') {
return { ...empty, scheduleType: 'weekly', runTime, weekDays: String(week).split(',').filter(Boolean) }
}
if (day !== '*' && day !== '?' && month === '*' && (week === '?' || week === '*')) {
return { ...empty, scheduleType: 'monthly', runTime, monthDay: Number(day) || 1 }
}
return empty
}
function scheduleLabel(cron) {
const text = String(cron || '').trim()
if (!text) return '-'
const s = parseCronToSchedule(text)
if (s.scheduleType === 'once' && s.runAt) return '指定 ' + String(s.runAt).slice(0, 16)
if (s.scheduleType === 'daily') return '每天 ' + s.runTime
if (s.scheduleType === 'weekly') {
const names = (s.weekDays || []).map(v => weekLabelMap[v] || v).join('/')
return '每周 ' + (names || '-') + ' ' + s.runTime
}
if (s.scheduleType === 'monthly') return '每月 ' + s.monthDay + '号 ' + s.runTime
return text
}
function onScheduleTypeChange(type) {
if (type === 'none') form.value.status = '1'
}
function applyScheduleToForm() {
Object.assign(schedule, parseCronToSchedule(form.value.cronExpression))
}
function queryList(params) {
return listMigration(params)
}
function resetForm() {
form.value = {
migrationId: undefined,
migrationName: '',
sqlText: '',
cronExpression: '',
status: '1',
remark: ''
}
Object.assign(schedule, parseCronToSchedule(''))
formRef.value?.clearValidate?.()
}
function handleAdd() {
resetForm()
title.value = '新增数据迁移'
open.value = true
}
async function handleUpdate(row) {
resetForm()
const id = row?.migrationId
const res = await getMigration(id)
form.value = { ...(res.data || res) }
applyScheduleToForm()
title.value = '修改数据迁移'
open.value = true
}
function validateSchedule() {
const type = schedule.scheduleType
if (type === 'none') return true
if (type === 'once' && !schedule.runAt) {
proxy.$modal.msgError('请选择执行时间')
return false
}
if (type === 'weekly' && !(schedule.weekDays || []).length) {
proxy.$modal.msgError('请选择星期')
return false
}
if (['daily', 'weekly', 'monthly'].includes(type) && !schedule.runTime) {
proxy.$modal.msgError('请选择时间')
return false
}
if (!buildCronFromSchedule(schedule)) {
proxy.$modal.msgError('执行计划无效')
return false
}
return true
}
function submitForm() {
formRef.value?.validate?.(async (valid) => {
if (!valid) return
if (!validateSchedule()) return
const cron = buildCronFromSchedule(schedule)
form.value.cronExpression = cron
if (!cron) form.value.status = '1'
saving.value = true
try {
if (form.value.migrationId) await updateMigration(form.value)
else await addMigration(form.value)
proxy.$modal.msgSuccess('保存成功')
open.value = false
tableRef.value?.refresh?.()
} finally {
saving.value = false
}
})
}
function handleDelete(ids) {
const idList = Array.isArray(ids) ? ids : [ids]
proxy.$modal.confirm('确认删除选中脚本?').then(async () => {
await delMigration(idList.join(','))
proxy.$modal.msgSuccess('删除成功')
tableRef.value?.refresh?.()
}).catch(() => {})
}
function handleRun(row) {
proxy.$modal.confirm('确认立即执行该迁移脚本?').then(async () => {
const res = await runMigration(row.migrationId)
const data = res.data || res
proxy.$modal.msgSuccess('执行完成:影响 ' + (data.affectedRows ?? 0) + ' 行')
tableRef.value?.refresh?.()
}).catch(() => {})
}
async function openLog(row) {
currentMigrationId.value = row.migrationId
logOpen.value = true
logLoading.value = true
try {
const res = await listMigrationLog({ migrationId: row.migrationId, pageNum: 1, pageSize: 100 })
logRows.value = res.rows || []
} finally {
logLoading.value = false
}
}
</script>