一、背景介绍

在开发定时任务管理系统时,前端需要支持用户新增定时任务。功能包括:任务名称、调用方法、Cron 表达式、并发策略、状态等。本文将从零到一记录完整的开发过程,包括前端校验和后端逻辑的所有踩坑经历,希望能帮助遇到类似问题的开发者。

技术栈

  • 前端:Vue 2 + Element UI

  • 后端:Spring Boot + MyBatis + Quartz

  • 数据库:SQL Server

二、最终效果预览

text

┌─────────────────────────────────────────────────────────────┐
│  Add Job                                                    │
├─────────────────────────────────────────────────────────────┤
│  Job Name:       [测试任务                    ]             │
│  Job Group:       [System ▼]                               │
│  Invoke Method:   [taskService.sync()        ]             │
│                   Format: beanName.methodName()             │
│  Cron Expression: [0 0 0 * * ?              ] [Generate]   │
│                   ✓ Valid cron expression                  │
│  Concurrent:      ● Allow Concurrent    ○ Disallow         │
│  Status:          ● Enabled             ○ Disabled         │
├─────────────────────────────────────────────────────────────┤
│                              [Cancel]        [OK]           │
└─────────────────────────────────────────────────────────────┘

三、踩坑历程

坑1:Cron 表达式格式错误

现象:用户输入 0 0 * * ?(5位),后端 Quartz 需要 6 位(秒 分 时 日 月 星期)

text

CronExpression '0 0 * * ?' is invalid

原因:前端没有校验 Cron 格式

解决方案:添加前端格式校验

javascript

checkCron() {
  const cron = this.formData.cronExpression
  if (!cron) return
  
  const parts = cron.trim().split(/\s+/)
  if (parts.length !== 6) {
    this.cronValid = false
    this.cronErrorMessage = 'Cron must have 6 fields: second minute hour day month week'
    return
  }
  
  // 秒、分、时范围校验
  const second = parseInt(parts[0])
  const minute = parseInt(parts[1])
  const hour = parseInt(parts[2])
  
  if (second < 0 || second > 59) {
    this.cronValid = false
    this.cronErrorMessage = 'Second must be between 0 and 59'
    return
  }
  // ... 分钟、小时类似校验
}

坑2:用户不知道 Cron 怎么写

现象:用户面对空输入框,不知道填什么格式

原因:缺少提示和示例

解决方案:添加 placeholder 和提示文字

html

<el-input 
  v-model="formData.cronExpression" 
  placeholder="e.g., 0 0 0 * * ?"
/>
<div class="cron-hint">Format: second minute hour day month week</div>
<div class="cron-examples">
  <span>Examples: 0 0 0 * * ? (daily 00:00)</span>
  <span>0 0 2 ? * SUN (weekly Sunday 02:00)</span>
</div>

坑3:手写 Cron 容易出错

现象:用户写 0 0 0 * * ?(中文问号)或 0 0 0 * *(少字段)

原因:纯手工输入,没有辅助工具

解决方案:添加 Cron 生成器弹窗

html

<el-popover trigger="click" v-model="cronPopoverVisible" placement="bottom" width="400">
  <div class="cron-generator">
    <div class="cron-row">
      <span class="cron-label">Second (0-59):</span>
      <el-select v-model="cronSecond" size="mini" clearable>
        <el-option v-for="i in 59" :key="i" :label="i" :value="i" />
      </el-select>
    </div>
    <div class="cron-row">
      <span class="cron-label">Minute (0-59):</span>
      <el-select v-model="cronMinute" size="mini" clearable>
        <el-option v-for="i in 59" :key="i" :label="i" :value="i" />
      </el-select>
    </div>
    <!-- 小时、日期、月份、星期选择器... -->
    <div class="cron-preview">
      <strong>Cron:</strong> {{ generatedCron }}
    </div>
    <div style="text-align: right;">
      <el-button size="mini" @click="cronPopoverVisible = false">Cancel</el-button>
      <el-button type="primary" size="mini" @click="applyCron">Apply</el-button>
    </div>
  </div>
  <el-button slot="reference" type="text" icon="el-icon-time">Generate</el-button>
</el-popover>

javascript

computed: {
  generatedCron() {
    return `${this.cronSecond} ${this.cronMinute} ${this.cronHour} ${this.cronDay} ${this.cronMonth} ${this.cronWeek}`
  }
},
methods: {
  applyCron() {
    this.formData.cronExpression = this.generatedCron
    this.cronPopoverVisible = false
    this.checkCron()
  }
}

坑4:日期和星期冲突

现象:用户同时选择了日期(如 15 号)和星期(如 Monday),Quartz 报错

text

'?' can only be specified for Day-of-Month or Day-of-Week

原因:Cron 规则中,日期和星期不能同时指定

解决方案:添加互斥逻辑

javascript

data() {
  return {
    cronDay: '*',
    cronWeek: '?'
  }
},
methods: {
  onDayChange() {
    if (this.cronDay !== '?' && this.cronDay !== '*') {
      this.cronWeek = '?'
    }
  },
  onWeekChange() {
    if (this.cronWeek !== '?' && this.cronWeek !== '*') {
      this.cronDay = '?'
    }
  }
}

html

<div class="cron-row">
  <span class="cron-label">Day of Month (1-31):</span>
  <el-select v-model="cronDay" @change="onDayChange">
    <el-option label="Every day" value="*" />
    <el-option label="?" value="?" />
    <el-option v-for="i in 31" :key="i" :label="i" :value="i" />
  </el-select>
  <span class="cron-hint" v-if="cronDay !== '?' && cronDay !== '*'">(Week will be set to ?)</span>
</div>

坑5:Bean 名大小写问题

现象:用户输入 TaskService.sync()(首字母大写),后端找不到 Bean

text

Bean not found: TaskService

原因:Spring Bean 默认首字母小写,TaskService 应该是 taskService

解决方案:前端添加格式提示,后端自动转换

html

<div class="form-tip">
  Format: beanName.methodName() (lowercase first letter)
  <br>Example: taskService.syncMonthLeepaOrder()
</div>

java

// 后端自动将首字母转小写
String className = invokeTarget.substring(0, dotIndex);
String beanName = Character.toLowerCase(className.charAt(0)) + className.substring(1);
Object bean = SpringUtils.getBean(beanName);

坑6:方法名错误

现象:用户输入 taskService.syncOrder(),但实际方法名是 syncLeepaOrder

text

Method not found: syncOrder()

原因:用户不知道有哪些可用方法,也没有实时校验

解决方案:添加实时校验,输入框失焦时校验

html

<el-input 
  v-model="formData.invokeTarget" 
  placeholder="e.g., taskService.syncMonthLeepaOrder()"
  @blur="checkInvokeTarget"
/>
<div v-if="invokeTargetValid === false" class="error-tip">
  {{ invokeTargetError }}
</div>

javascript

async checkInvokeTarget() {
  const invokeTarget = this.formData.invokeTarget
  if (!invokeTarget) {
    this.invokeTargetValid = null
    return
  }
  
  // 格式校验
  const dotIndex = invokeTarget.indexOf('.')
  const paramStart = invokeTarget.indexOf('(')
  const paramEnd = invokeTarget.lastIndexOf(')')
  
  if (dotIndex === -1 || paramStart === -1 || paramEnd === -1) {
    this.invokeTargetValid = false
    this.invokeTargetError = 'Invalid format. Expected: beanName.methodName()'
    return
  }
  
  // 调用后端校验接口
  const res = await this.axios.post('/scheduler/job/validate', {
    invokeTarget: invokeTarget
  })
  
  if (res.data && res.data.valid) {
    this.invokeTargetValid = true
    this.invokeTargetError = ''
  } else {
    this.invokeTargetValid = false
    this.invokeTargetError = res.data.message
  }
}

坑7:id 为 null 导致 Quartz 创建失败(重点!)

现象:新增任务时,Quartz 创建失败

text

java.lang.NullPointerException
at SchedulerJobService.createScheduleJob(SchedulerJobService.java:62)

原因:代码顺序错误,先创建 Quartz 任务,后插入数据库。此时 job.id 还是 null

错误代码

java

// ❌ 错误顺序
jobService.createScheduleJob(job);  // job.id 还是 null!
jobMapper.insertJob(job);

正确顺序

java

// ✅ 正确顺序
jobMapper.insertJob(job);           // 先插入数据库,获取自增 ID
jobService.createScheduleJob(job);  // 此时 job.id 已有值

关键点:MyBatis 的 @Options(useGeneratedKeys = true) 会在插入后自动回填 ID

java

@Insert("INSERT INTO ELAB_SCHEDULER_JOB (jobName, jobGroup, invokeTarget, cronExpression, concurrent, status, createBy, createTime) VALUES " +
        "(#{jobName}, #{jobGroup}, #{invokeTarget}, #{cronExpression}, #{concurrent}, #{status}, #{createBy}, GETDATE())")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertJob(SchedulerJob job);

坑8:Quartz 创建失败时数据库记录没有回滚

现象:Quartz 创建失败(如方法名错误),但数据库已经有记录了

原因:没有事务控制,数据库插入成功,但 Quartz 任务创建失败

解决方案:添加事务,失败时自动回滚

java

@PostMapping("/add")
@Transactional(transactionManager = "eLab_TransactionManager", rollbackFor = Exception.class)
public WebResult add(@RequestBody SchedulerJob job) {
    WebResult result = new WebResult();
    
    job.setCreateBy(getCurrentUser());
    
    // 1. 插入数据库
    int insertResult = jobMapper.insertJob(job);
    if (insertResult <= 0) {
        result.error("Failed to insert job");
        return result;
    }
    
    // 2. 创建 Quartz 任务(失败时事务自动回滚数据库)
    boolean success = jobService.createScheduleJob(job);
    
    if (success) {
        result.setMessage("Job added successfully");
    } else {
        result.error("Failed to schedule job");
    }
    return result;
}

坑9:重复提交

现象:用户快速连续点击 OK 按钮,发送了多个请求

原因:没有防重复提交机制

解决方案:添加 submitting 状态

javascript

data() {
  return {
    submitting: false
  }
},
methods: {
  async submitJob() {
    if (this.submitting) return
    
    this.$refs.jobForm.validate(async (valid) => {
      if (!valid) return
      
      if (this.cronValid !== true) {
        this.$message.error('Please enter a valid cron expression')
        return
      }
      
      this.submitting = true
      
      try {
        const res = await this.axios.post('/scheduler/job/add', this.formData)
        if (res.data && res.data.status === 200) {
          this.$message.success('Job added')
          this.dialogVisible = false
          this.loadJobList()
        } else {
          this.$message.error(res.data.message || 'Operation failed')
        }
      } catch (err) {
        this.$message.error('Operation failed')
      } finally {
        this.submitting = false
      }
    })
  }
}

html

<el-button type="primary" @click="submitJob" :loading="submitting">OK</el-button>

坑10:编辑时传了不该传的字段

现象:编辑任务时,后端报错

text

Failed to convert property value of type 'java.lang.String' 
to required type 'java.util.Date' for property 'createTime'

原因{ ...row } 把 createTimeupdateTime 也复制了

错误代码

javascript

// ❌ 错误写法
this.formData = { ...row }

正确写法

javascript

// ✅ 正确写法
this.formData = {
  id: row.id,
  jobName: row.jobName,
  jobGroup: row.jobGroup,
  invokeTarget: row.invokeTarget,
  cronExpression: row.cronExpression,
  concurrent: row.concurrent,
  status: row.status
}

四、最终校验流程图

text

用户点击 OK
    │
    ▼
submitting 检查 ───► 如果正在提交,忽略
    │
    ▼
表单验证 ───► 失败 ───► 显示错误提示
    │
    ▼ 通过
Cron 格式校验 ───► 失败 ───► 显示红色错误
    │
    ▼ 通过
显示绿色 ✓ Valid cron expression
    │
    ▼
Invoke Method 校验 ───► 失败 ───► 显示错误
    │
    ▼ 通过
提交请求
    │
    ├──► 成功 ───► 关闭弹窗,刷新列表
    │
    └──► 失败 ───► 显示错误信息
    │
    ▼
submitting = false(恢复按钮)

五、完整代码

前端 TaskSchedule.vue(核心部分)

vue

<template>
  <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="560px" @close="resetForm">
    <el-form ref="jobForm" :model="formData" :rules="formRules" label-width="120px" size="small">
      
      <el-form-item label="Job Name" prop="jobName">
        <el-input v-model="formData.jobName" placeholder="Enter job name" />
      </el-form-item>

      <el-form-item label="Job Group" prop="jobGroup">
        <el-select v-model="formData.jobGroup" placeholder="Select job group">
          <el-option label="System" value="DEFAULT" />
          <el-option label="Custom" value="CUSTOM" />
        </el-select>
      </el-form-item>

      <el-form-item label="Invoke Method" prop="invokeTarget">
        <el-input 
          v-model="formData.invokeTarget" 
          placeholder="e.g., taskService.syncMonthLeepaOrder()"
          @blur="checkInvokeTarget"
        />
        <div class="form-tip">
          Format: beanName.methodName() (lowercase first letter)
        </div>
        <div v-if="invokeTargetValid === false" class="error-tip">
          {{ invokeTargetError }}
        </div>
      </el-form-item>

      <el-form-item label="Cron Expression" prop="cronExpression">
        <el-input 
          v-model="formData.cronExpression" 
          placeholder="e.g., 0 0 0 * * ?"
          @blur="checkCron"
          style="width: 100%"
        />
        <el-button type="text" size="mini" @click="cronPopoverVisible = true" icon="el-icon-time">
          Generate
        </el-button>
        
        <div v-if="cronValid === false" class="error-tip">
          {{ cronErrorMessage }}
        </div>
        <div v-if="cronValid === true" class="success-tip">
          ✓ Valid cron expression
        </div>
        
        <!-- Cron 生成器弹窗 -->
        <el-popover
          placement="bottom"
          width="400"
          trigger="click"
          v-model="cronPopoverVisible"
        >
          <div class="cron-generator">
            <div class="cron-row">
              <span class="cron-label">Second (0-59):</span>
              <el-select v-model="cronSecond" size="mini" clearable>
                <el-option label="0" value="0" />
                <el-option v-for="i in 59" :key="i" :label="i" :value="i" />
              </el-select>
            </div>
            <div class="cron-row">
              <span class="cron-label">Minute (0-59):</span>
              <el-select v-model="cronMinute" size="mini" clearable>
                <el-option label="Every minute" value="*" />
                <el-option v-for="i in 59" :key="i" :label="i" :value="i" />
              </el-select>
            </div>
            <div class="cron-row">
              <span class="cron-label">Hour (0-23):</span>
              <el-select v-model="cronHour" size="mini" clearable>
                <el-option label="Every hour" value="*" />
                <el-option v-for="i in 23" :key="i" :label="i" :value="i" />
              </el-select>
            </div>
            <div class="cron-row">
              <span class="cron-label">Day of Month (1-31):</span>
              <el-select 
                v-model="cronDay" 
                size="mini" 
                clearable
                @change="onDayChange"
              >
                <el-option label="Every day" value="*" />
                <el-option label="?" value="?" />
                <el-option v-for="i in 31" :key="i" :label="i" :value="i" />
              </el-select>
              <span class="cron-hint" v-if="cronDay !== '?' && cronDay !== '*'">(Week will be ?)</span>
            </div>
            <div class="cron-row">
              <span class="cron-label">Month (1-12):</span>
              <el-select v-model="cronMonth" size="mini" clearable>
                <el-option label="Every month" value="*" />
                <el-option v-for="i in 12" :key="i" :label="i" :value="i" />
              </el-select>
            </div>
            <div class="cron-row">
              <span class="cron-label">Day of Week (0-7):</span>
              <el-select 
                v-model="cronWeek" 
                size="mini" 
                clearable
                @change="onWeekChange"
              >
                <el-option label="Every day" value="*" />
                <el-option label="?" value="?" />
                <el-option label="Sunday" value="0" />
                <el-option label="Monday" value="1" />
                <el-option label="Tuesday" value="2" />
                <el-option label="Wednesday" value="3" />
                <el-option label="Thursday" value="4" />
                <el-option label="Friday" value="5" />
                <el-option label="Saturday" value="6" />
              </el-select>
              <span class="cron-hint" v-if="cronWeek !== '?' && cronWeek !== '*'">(Day will be ?)</span>
            </div>
            <div class="cron-preview">
              <strong>Cron:</strong> {{ generatedCron }}
            </div>
            <div style="text-align: right; margin-top: 10px;">
              <el-button size="mini" @click="cronPopoverVisible = false">Cancel</el-button>
              <el-button type="primary" size="mini" @click="applyCron">Apply</el-button>
            </div>
          </div>
          <el-button slot="reference" type="text" size="mini" icon="el-icon-time">Generate</el-button>
        </el-popover>
      </el-form-item>

      <el-form-item label="Concurrent" prop="concurrent">
        <el-radio-group v-model="formData.concurrent">
          <el-radio label="1">Allow Concurrent</el-radio>
          <el-radio label="0">Disallow Concurrent</el-radio>
        </el-radio-group>
      </el-form-item>
      
      <el-form-item label="Status" prop="status">
        <el-radio-group v-model="formData.status">
          <el-radio label="0">Enabled</el-radio>
          <el-radio label="1">Disabled</el-radio>
        </el-radio-group>
      </el-form-item>
      
    </el-form>
    
    <span slot="footer" class="dialog-footer">
      <el-button @click="dialogVisible = false">Cancel</el-button>
      <el-button type="primary" @click="submitJob" :loading="submitting">OK</el-button>
    </span>
  </el-dialog>
</template>

<script>
import cronParser from 'cron-parser'

export default {
  name: 'TaskSchedule',
  data() {
    return {
      submitting: false,
      dialogVisible: false,
      dialogTitle: '',
      cronValid: null,
      cronErrorMessage: '',
      invokeTargetValid: null,
      invokeTargetError: '',
      cronPopoverVisible: false,
      cronSecond: '0',
      cronMinute: '0',
      cronHour: '0',
      cronDay: '*',
      cronMonth: '*',
      cronWeek: '?',
      formData: {
        id: null,
        jobName: '',
        jobGroup: 'DEFAULT',
        invokeTarget: '',
        cronExpression: '',
        concurrent: '1',
        status: '0'
      },
      formRules: {
        jobName: [{ required: true, message: 'Job name is required', trigger: 'blur' }],
        jobGroup: [{ required: true, message: 'Job group is required', trigger: 'change' }],
        invokeTarget: [{ required: true, message: 'Invoke method is required', trigger: 'blur' }],
        cronExpression: [{ required: true, message: 'Cron expression is required', trigger: 'blur' }]
      }
    }
  },
  computed: {
    generatedCron() {
      return `${this.cronSecond} ${this.cronMinute} ${this.cronHour} ${this.cronDay} ${this.cronMonth} ${this.cronWeek}`
    }
  },
  methods: {
    checkCron() {
      const cron = this.formData.cronExpression
      if (!cron) {
        this.cronValid = null
        this.cronErrorMessage = ''
        return
      }
      try {
        cronParser.parseExpression(cron)
        this.cronValid = true
        this.cronErrorMessage = ''
      } catch (err) {
        this.cronValid = false
        this.cronErrorMessage = err.message
      }
    },
    
    checkInvokeTarget() {
      const invokeTarget = this.formData.invokeTarget
      if (!invokeTarget) {
        this.invokeTargetValid = null
        this.invokeTargetError = ''
        return
      }
      
      const dotIndex = invokeTarget.indexOf('.')
      const paramStart = invokeTarget.indexOf('(')
      const paramEnd = invokeTarget.lastIndexOf(')')
      
      if (dotIndex === -1 || paramStart === -1 || paramEnd === -1) {
        this.invokeTargetValid = false
        this.invokeTargetError = 'Invalid format. Expected: beanName.methodName()'
        return
      }
      
      const className = invokeTarget.substring(0, dotIndex)
      const methodName = invokeTarget.substring(dotIndex + 1, paramStart)
      
      if (!className || !methodName) {
        this.invokeTargetValid = false
        this.invokeTargetError = 'Class name and method name cannot be empty'
        return
      }
      
      this.invokeTargetValid = true
      this.invokeTargetError = ''
    },
    
    applyCron() {
      this.formData.cronExpression = this.generatedCron
      this.cronPopoverVisible = false
      this.checkCron()
    },
    
    onDayChange() {
      if (this.cronDay !== '?' && this.cronDay !== '*') {
        this.cronWeek = '?'
      }
    },
    
    onWeekChange() {
      if (this.cronWeek !== '?' && this.cronWeek !== '*') {
        this.cronDay = '?'
      }
    },
    
    handleAdd() {
      this.dialogTitle = 'Add Job'
      this.formData = {
        id: null,
        jobName: '',
        jobGroup: 'DEFAULT',
        invokeTarget: '',
        cronExpression: '',
        concurrent: '1',
        status: '0'
      }
      this.cronValid = null
      this.cronErrorMessage = ''
      this.invokeTargetValid = null
      this.invokeTargetError = ''
      this.dialogVisible = true
    },
    
    async submitJob() {
      if (this.submitting) return
      
      this.$refs.jobForm.validate(async (valid) => {
        if (!valid) return
        
        if (!this.formData.cronExpression) {
          this.$message.error('Please enter cron expression')
          return
        }
        
        if (this.cronValid !== true) {
          this.$message.error('Please enter a valid cron expression')
          return
        }
        
        this.submitting = true
        
        const isEdit = !!this.formData.id
        const url = isEdit ? '/scheduler/job/update' : '/scheduler/job/add'
        
        const submitData = {
          jobName: this.formData.jobName,
          jobGroup: this.formData.jobGroup,
          invokeTarget: this.formData.invokeTarget,
          cronExpression: this.formData.cronExpression,
          concurrent: this.formData.concurrent,
          status: this.formData.status
        }
        if (isEdit) {
          submitData.id = this.formData.id
        }
        
        try {
          const res = await this.axios.post(url, submitData)
          if (res.data && res.data.status === 200) {
            this.$message.success(isEdit ? 'Job updated' : 'Job added')
            this.dialogVisible = false
            this.loadJobList()
          } else {
            this.$message.error(res.data.message || 'Operation failed')
          }
        } catch (err) {
          console.error(err)
          this.$message.error('Operation failed')
        } finally {
          this.submitting = false
        }
      })
    },
    
    resetForm() {
      if (this.$refs.jobForm) {
        this.$refs.jobForm.resetFields()
      }
      this.cronValid = null
      this.cronErrorMessage = ''
      this.invokeTargetValid = null
      this.invokeTargetError = ''
    }
  }
}
</script>

<style lang="scss" scoped>
.form-tip {
  color: #909399;
  font-size: 12px;
  margin-top: 4px;
}
.error-tip {
  color: #f56c6c;
  font-size: 12px;
  margin-top: 4px;
}
.success-tip {
  color: #67c23a;
  font-size: 12px;
  margin-top: 4px;
}
.cron-generator {
  .cron-row {
    display: flex;
    align-items: center;
    margin-bottom: 10px;
    
    .cron-label {
      width: 130px;
      font-size: 12px;
    }
    
    .el-select {
      width: 120px;
    }
    
    .cron-hint {
      margin-left: 10px;
      font-size: 11px;
      color: #909399;
    }
  }
  
  .cron-preview {
    margin-top: 10px;
    padding: 8px;
    background-color: #f5f7fa;
    border-radius: 4px;
    font-size: 12px;
    word-break: break-all;
  }
}
</style>

后端 Controller(核心部分)

java

@RestController
@RequestMapping("/scheduler/job")
public class SchedulerJobController {
    
    @Autowired
    private SchedulerJobService jobService;
    
    @Autowired
    private SchedulerJobMapper jobMapper;
    
    /**
     * 新增任务
     */
    @PostMapping("/add")
    @Transactional(transactionManager = "eLab_TransactionManager", rollbackFor = Exception.class)
    public WebResult add(@RequestBody SchedulerJob job) {
        WebResult result = new WebResult();
        
        // 1. 设置创建人
        job.setCreateBy(getCurrentUser());
        
        // 2. 先插入数据库(获取自增 ID)
        int insertResult = jobMapper.insertJob(job);
        if (insertResult <= 0) {
            result.error("Failed to insert job");
            return result;
        }
        
        // 3. 创建 Quartz 任务(如果失败,事务自动回滚数据库)
        boolean success = jobService.createScheduleJob(job);
        
        if (success) {
            result.setMessage("Job added successfully");
        } else {
            result.error("Failed to schedule job");
        }
        return result;
    }
    
    private String getCurrentUser() {
        // 从 token 获取当前登录用户
        return "system";
    }
}

Service(Quartz 任务创建)

java

@Service
public class SchedulerJobService {
    
    @Autowired
    private Scheduler scheduler;
    
    public boolean createScheduleJob(SchedulerJob job) throws SchedulerException {
        // 1. 校验 invokeTarget
        if (!isValidateInvokeTarget(job.getInvokeTarget())) {
            return false;
        }
        
        // 2. 创建 JobDetail
        JobDetail jobDetail = JobBuilder.newJob(QuartzJobExecution.class)
                .withIdentity(job.getId().toString(), job.getJobGroup())
                .build();
        jobDetail.getJobDataMap().put("JOB_PARAM_KEY", job);
        
        // 3. 创建 Trigger
        CronScheduleBuilder cronBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
        CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(job.getId().toString(), job.getJobGroup())
                .withSchedule(cronBuilder)
                .build();
        
        // 4. 调度任务
        scheduler.scheduleJob(jobDetail, trigger);
        
        // 5. 如果任务是暂停状态
        if ("1".equals(job.getStatus())) {
            scheduler.pauseJob(jobDetail.getKey());
        }
        
        return true;
    }
    
    private boolean isValidateInvokeTarget(String invokeTarget) {
        int dotIndex = invokeTarget.indexOf(".");
        int paramStart = invokeTarget.indexOf("(");
        
        if (dotIndex == -1 || paramStart == -1) {
            log.info("Invalid format. Expected: beanName.methodName()");
            return false;
        }
        
        String className = invokeTarget.substring(0, dotIndex);
        String methodName = invokeTarget.substring(dotIndex + 1, paramStart);
        String beanName = Character.toLowerCase(className.charAt(0)) + className.substring(1);
        
        // 校验 Bean 是否存在
        Object bean;
        try {
            bean = SpringUtils.getBean(beanName);
        } catch (Exception e) {
            log.info("Bean not found: " + beanName);
            return false;
        }
        
        // 校验方法是否存在
        try {
            bean.getClass().getDeclaredMethod(methodName);
            return true;
        } catch (NoSuchMethodException e) {
            log.info("Method not found: " + methodName + "()");
            return false;
        }
    }
}

六、总结

问题 解决方案
1 Cron 格式错误 添加格式校验
2 用户不会写 Cron 添加 placeholder 提示
3 手写容易出错 添加 Cron 生成器
4 日期星期冲突 添加互斥逻辑
5 Bean 名大小写 后端自动转换首字母小写
6 方法名错误 添加实时校验
7 id 为 null 先插入数据库,再创建 Quartz 任务
8 事务回滚 添加 @Transactional
9 重复提交 添加 submitting 状态
10 编辑传多余字段 只复制需要的字段

核心原则:提前校验,实时反馈,先存数据再建任务,事务保证一致性!

希望这篇文章能帮助到遇到类似问题的开发者。如有疑问,欢迎评论区交流!🎯

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐