新增数据迁移任务到定时任务白名单;优化默认收货时间范围逻辑
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user