新增数据迁移任务到定时任务白名单;优化默认收货时间范围逻辑
This commit is contained in:
+204
@@ -0,0 +1,204 @@
|
|||||||
|
package com.bms.web.controller.tool;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.quartz.SchedulerException;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import com.bms.common.annotation.Log;
|
||||||
|
import com.bms.common.constant.ScheduleConstants;
|
||||||
|
import com.bms.common.core.controller.BaseController;
|
||||||
|
import com.bms.common.core.domain.AjaxResult;
|
||||||
|
import com.bms.common.core.page.TableDataInfo;
|
||||||
|
import com.bms.common.enums.BusinessType;
|
||||||
|
import com.bms.common.exception.ServiceException;
|
||||||
|
import com.bms.common.exception.job.TaskException;
|
||||||
|
import com.bms.common.utils.StringUtils;
|
||||||
|
import com.bms.quartz.domain.SysJob;
|
||||||
|
import com.bms.quartz.service.ISysJobService;
|
||||||
|
import com.bms.quartz.util.CronUtils;
|
||||||
|
import com.bms.system.domain.SysDataMigration;
|
||||||
|
import com.bms.system.domain.SysDataMigrationLog;
|
||||||
|
import com.bms.system.service.ISysDataMigrationService;
|
||||||
|
|
||||||
|
/** 系统工具 - 数据迁移:保存 SQL、手动/定时执行(Quartz) */
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/tool/migration")
|
||||||
|
public class DataMigrationController extends BaseController
|
||||||
|
{
|
||||||
|
private static final String JOB_GROUP = "DATA_MIGRATION";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ISysDataMigrationService migrationService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ISysJobService jobService;
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('tool:migration:list')")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public TableDataInfo list(SysDataMigration query)
|
||||||
|
{
|
||||||
|
startPage();
|
||||||
|
List<SysDataMigration> list = migrationService.selectSysDataMigrationList(query);
|
||||||
|
return getDataTable(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('tool:migration:query')")
|
||||||
|
@GetMapping("/{migrationId}")
|
||||||
|
public AjaxResult getInfo(@PathVariable Long migrationId)
|
||||||
|
{
|
||||||
|
return success(migrationService.selectSysDataMigrationById(migrationId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('tool:migration:add')")
|
||||||
|
@Log(title = "数据迁移", businessType = BusinessType.INSERT)
|
||||||
|
@PostMapping
|
||||||
|
public AjaxResult add(@Validated @RequestBody SysDataMigration row) throws SchedulerException, TaskException
|
||||||
|
{
|
||||||
|
row.setCreateBy(getUsername());
|
||||||
|
migrationService.insertSysDataMigration(row);
|
||||||
|
syncJob(row);
|
||||||
|
return success(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('tool:migration:edit')")
|
||||||
|
@Log(title = "数据迁移", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping
|
||||||
|
public AjaxResult edit(@Validated @RequestBody SysDataMigration row) throws SchedulerException, TaskException
|
||||||
|
{
|
||||||
|
row.setUpdateBy(getUsername());
|
||||||
|
migrationService.updateSysDataMigration(row);
|
||||||
|
SysDataMigration fresh = migrationService.selectSysDataMigrationById(row.getMigrationId());
|
||||||
|
syncJob(fresh);
|
||||||
|
return success(fresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('tool:migration:remove')")
|
||||||
|
@Log(title = "数据迁移", businessType = BusinessType.DELETE)
|
||||||
|
@DeleteMapping("/{migrationIds}")
|
||||||
|
public AjaxResult remove(@PathVariable Long[] migrationIds) throws SchedulerException
|
||||||
|
{
|
||||||
|
if (migrationIds != null)
|
||||||
|
{
|
||||||
|
for (Long id : migrationIds)
|
||||||
|
{
|
||||||
|
SysDataMigration row = migrationService.selectSysDataMigrationById(id);
|
||||||
|
if (row != null)
|
||||||
|
{
|
||||||
|
removeJob(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return toAjax(migrationService.deleteSysDataMigrationByIds(migrationIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('tool:migration:run')")
|
||||||
|
@Log(title = "数据迁移执行", businessType = BusinessType.OTHER)
|
||||||
|
@PostMapping("/run/{migrationId}")
|
||||||
|
public AjaxResult run(@PathVariable Long migrationId)
|
||||||
|
{
|
||||||
|
Map<String, Object> result = migrationService.executeMigration(migrationId, "MANUAL", getUsername());
|
||||||
|
return success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('tool:migration:log')")
|
||||||
|
@GetMapping("/log/list")
|
||||||
|
public TableDataInfo logList(SysDataMigrationLog query)
|
||||||
|
{
|
||||||
|
startPage();
|
||||||
|
List<SysDataMigrationLog> list = migrationService.selectSysDataMigrationLogList(query);
|
||||||
|
return getDataTable(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncJob(SysDataMigration row) throws SchedulerException, TaskException
|
||||||
|
{
|
||||||
|
if (row == null || row.getMigrationId() == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean enable = "0".equals(row.getStatus()) && StringUtils.isNotEmpty(row.getCronExpression());
|
||||||
|
if (enable && !CronUtils.isValid(row.getCronExpression()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("Cron 表达式不正确");
|
||||||
|
}
|
||||||
|
if (!enable)
|
||||||
|
{
|
||||||
|
removeJob(row);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String invokeTarget = "dataMigrationTask.executeById(" + row.getMigrationId() + "L)";
|
||||||
|
String jobName = "data-migration-" + row.getMigrationId();
|
||||||
|
if (row.getJobId() != null)
|
||||||
|
{
|
||||||
|
SysJob existed = jobService.selectJobById(row.getJobId());
|
||||||
|
if (existed != null)
|
||||||
|
{
|
||||||
|
existed.setJobName(jobName);
|
||||||
|
existed.setJobGroup(JOB_GROUP);
|
||||||
|
existed.setInvokeTarget(invokeTarget);
|
||||||
|
existed.setCronExpression(row.getCronExpression());
|
||||||
|
existed.setMisfirePolicy("3");
|
||||||
|
existed.setConcurrent("1");
|
||||||
|
existed.setUpdateBy(getUsername());
|
||||||
|
existed.setRemark(row.getMigrationName());
|
||||||
|
jobService.updateJob(existed);
|
||||||
|
existed.setStatus(ScheduleConstants.Status.NORMAL.getValue());
|
||||||
|
jobService.changeStatus(existed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SysJob job = new SysJob();
|
||||||
|
job.setJobName(jobName);
|
||||||
|
job.setJobGroup(JOB_GROUP);
|
||||||
|
job.setInvokeTarget(invokeTarget);
|
||||||
|
job.setCronExpression(row.getCronExpression());
|
||||||
|
job.setMisfirePolicy("3");
|
||||||
|
job.setConcurrent("1");
|
||||||
|
job.setCreateBy(getUsername());
|
||||||
|
job.setRemark(row.getMigrationName());
|
||||||
|
jobService.insertJob(job);
|
||||||
|
job.setStatus(ScheduleConstants.Status.NORMAL.getValue());
|
||||||
|
jobService.changeStatus(job);
|
||||||
|
|
||||||
|
SysDataMigration patch = new SysDataMigration();
|
||||||
|
patch.setMigrationId(row.getMigrationId());
|
||||||
|
patch.setJobId(job.getJobId());
|
||||||
|
patch.setUpdateBy(getUsername());
|
||||||
|
migrationService.updateSysDataMigration(patch);
|
||||||
|
row.setJobId(job.getJobId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeJob(SysDataMigration row) throws SchedulerException
|
||||||
|
{
|
||||||
|
if (row == null || row.getJobId() == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SysJob job = jobService.selectJobById(row.getJobId());
|
||||||
|
if (job != null)
|
||||||
|
{
|
||||||
|
jobService.deleteJob(job);
|
||||||
|
}
|
||||||
|
SysDataMigration patch = new SysDataMigration();
|
||||||
|
patch.setMigrationId(row.getMigrationId());
|
||||||
|
patch.setJobId(null);
|
||||||
|
// keep job_id clear: update XML only sets jobId when non-null; force via dedicated clear
|
||||||
|
clearJobId(row.getMigrationId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearJobId(Long migrationId)
|
||||||
|
{
|
||||||
|
migrationService.clearJobId(migrationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -164,10 +164,11 @@ public class Constants
|
|||||||
* 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)
|
* 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)
|
||||||
* com.bms.customer 覆盖客户/供应商资料同步、客户授信到期等业务任务包;
|
* com.bms.customer 覆盖客户/供应商资料同步、客户授信到期等业务任务包;
|
||||||
* com.bms.masterdata.task 为渠道产品主数据同步任务;com.bms.notify.task 为客户超额通知;
|
* com.bms.masterdata.task 为渠道产品主数据同步任务;com.bms.notify.task 为客户超额通知;
|
||||||
* com.bms.price.task 为价格/产品状态刷新任务
|
* com.bms.price.task 为价格/产品状态刷新任务;com.bms.system.migration 为数据迁移任务
|
||||||
*/
|
*/
|
||||||
public static final String[] JOB_WHITELIST_STR = { "com.bms.quartz.task", "com.bms.customer",
|
public static final String[] JOB_WHITELIST_STR = { "com.bms.quartz.task", "com.bms.customer",
|
||||||
"com.bms.masterdata.task", "com.bms.notify.task", "com.bms.price.task" };
|
"com.bms.masterdata.task", "com.bms.notify.task", "com.bms.price.task",
|
||||||
|
"com.bms.system.migration" };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定时任务违规的字符
|
* 定时任务违规的字符
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.bms.system.domain;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.bms.common.core.domain.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/** 数据迁移脚本 sys_data_migration */
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class SysDataMigration extends BaseEntity
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private Long migrationId;
|
||||||
|
private String migrationName;
|
||||||
|
private String sqlText;
|
||||||
|
private String cronExpression;
|
||||||
|
/** 0启用调度 / 1停用 */
|
||||||
|
private String status;
|
||||||
|
private Long jobId;
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date lastRunTime;
|
||||||
|
/** 0 ok / 1 fail */
|
||||||
|
private String lastRunStatus;
|
||||||
|
private String lastRunMessage;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.bms.system.domain;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.bms.common.core.domain.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/** 数据迁移执行日志 sys_data_migration_log */
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class SysDataMigrationLog extends BaseEntity
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private Long logId;
|
||||||
|
private Long migrationId;
|
||||||
|
/** MANUAL / CRON */
|
||||||
|
private String triggerType;
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date startTime;
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date endTime;
|
||||||
|
/** 0 ok / 1 fail */
|
||||||
|
private String status;
|
||||||
|
private Integer affectedRows;
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.bms.system.mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.bms.system.domain.SysDataMigration;
|
||||||
|
import com.bms.system.domain.SysDataMigrationLog;
|
||||||
|
|
||||||
|
public interface SysDataMigrationMapper
|
||||||
|
{
|
||||||
|
SysDataMigration selectSysDataMigrationById(Long migrationId);
|
||||||
|
|
||||||
|
List<SysDataMigration> selectSysDataMigrationList(SysDataMigration query);
|
||||||
|
|
||||||
|
int insertSysDataMigration(SysDataMigration row);
|
||||||
|
|
||||||
|
int updateSysDataMigration(SysDataMigration row);
|
||||||
|
|
||||||
|
int deleteSysDataMigrationByIds(Long[] migrationIds);
|
||||||
|
|
||||||
|
int clearJobId(Long migrationId);
|
||||||
|
|
||||||
|
int insertSysDataMigrationLog(SysDataMigrationLog row);
|
||||||
|
|
||||||
|
List<SysDataMigrationLog> selectSysDataMigrationLogList(SysDataMigrationLog query);
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package com.bms.system.migration.task;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import com.bms.system.service.ISysDataMigrationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quartz 调用目标:dataMigrationTask.executeById(1L)
|
||||||
|
*/
|
||||||
|
@Component("dataMigrationTask")
|
||||||
|
public class DataMigrationTask
|
||||||
|
{
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DataMigrationTask.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ISysDataMigrationService migrationService;
|
||||||
|
|
||||||
|
public void executeById(Long migrationId)
|
||||||
|
{
|
||||||
|
if (migrationId == null || migrationId.longValue() <= 0L)
|
||||||
|
{
|
||||||
|
log.warn("dataMigrationTask.executeById ignored: empty id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
migrationService.executeMigration(migrationId, "CRON", "quartz");
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package com.bms.system.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import com.bms.system.domain.SysDataMigration;
|
||||||
|
import com.bms.system.domain.SysDataMigrationLog;
|
||||||
|
|
||||||
|
public interface ISysDataMigrationService
|
||||||
|
{
|
||||||
|
SysDataMigration selectSysDataMigrationById(Long migrationId);
|
||||||
|
|
||||||
|
List<SysDataMigration> selectSysDataMigrationList(SysDataMigration query);
|
||||||
|
|
||||||
|
int insertSysDataMigration(SysDataMigration row);
|
||||||
|
|
||||||
|
int updateSysDataMigration(SysDataMigration row);
|
||||||
|
|
||||||
|
int deleteSysDataMigrationByIds(Long[] migrationIds);
|
||||||
|
|
||||||
|
int clearJobId(Long migrationId);
|
||||||
|
|
||||||
|
List<SysDataMigrationLog> selectSysDataMigrationLogList(SysDataMigrationLog query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run saved SQL. triggerType: MANUAL / CRON.
|
||||||
|
* @return summary map: status, affectedRows, message
|
||||||
|
*/
|
||||||
|
Map<String, Object> executeMigration(Long migrationId, String triggerType, String operator);
|
||||||
|
}
|
||||||
+233
@@ -0,0 +1,233 @@
|
|||||||
|
package com.bms.system.service.impl;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import com.bms.common.exception.ServiceException;
|
||||||
|
import com.bms.common.utils.StringUtils;
|
||||||
|
import com.bms.system.domain.SysDataMigration;
|
||||||
|
import com.bms.system.domain.SysDataMigrationLog;
|
||||||
|
import com.bms.system.mapper.SysDataMigrationMapper;
|
||||||
|
import com.bms.system.service.ISysDataMigrationService;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class SysDataMigrationServiceImpl implements ISysDataMigrationService
|
||||||
|
{
|
||||||
|
private static final Pattern DANGEROUS = Pattern.compile(
|
||||||
|
"(?i)\\b(drop\\s+database|drop\\s+schema|drop\\s+user|create\\s+user|grant\\b|revoke\\b|shutdown\\b|"
|
||||||
|
+ "load_file\\s*\\(|into\\s+outfile|into\\s+dumpfile|benchmark\\s*\\(|sleep\\s*\\()",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysDataMigrationMapper migrationMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SysDataMigration selectSysDataMigrationById(Long migrationId)
|
||||||
|
{
|
||||||
|
return migrationMapper.selectSysDataMigrationById(migrationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SysDataMigration> selectSysDataMigrationList(SysDataMigration query)
|
||||||
|
{
|
||||||
|
return migrationMapper.selectSysDataMigrationList(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int insertSysDataMigration(SysDataMigration row)
|
||||||
|
{
|
||||||
|
validateSql(row == null ? null : row.getSqlText());
|
||||||
|
if (row.getStatus() == null || row.getStatus().isEmpty())
|
||||||
|
{
|
||||||
|
row.setStatus("1");
|
||||||
|
}
|
||||||
|
return migrationMapper.insertSysDataMigration(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int updateSysDataMigration(SysDataMigration row)
|
||||||
|
{
|
||||||
|
if (row != null && row.getSqlText() != null)
|
||||||
|
{
|
||||||
|
validateSql(row.getSqlText());
|
||||||
|
}
|
||||||
|
return migrationMapper.updateSysDataMigration(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int deleteSysDataMigrationByIds(Long[] migrationIds)
|
||||||
|
{
|
||||||
|
return migrationMapper.deleteSysDataMigrationByIds(migrationIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int clearJobId(Long migrationId)
|
||||||
|
{
|
||||||
|
return migrationMapper.clearJobId(migrationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SysDataMigrationLog> selectSysDataMigrationLogList(SysDataMigrationLog query)
|
||||||
|
{
|
||||||
|
return migrationMapper.selectSysDataMigrationLogList(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> executeMigration(Long migrationId, String triggerType, String operator)
|
||||||
|
{
|
||||||
|
SysDataMigration row = migrationMapper.selectSysDataMigrationById(migrationId);
|
||||||
|
if (row == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("迁移脚本不存在");
|
||||||
|
}
|
||||||
|
validateSql(row.getSqlText());
|
||||||
|
|
||||||
|
Date start = new Date();
|
||||||
|
String status = "0";
|
||||||
|
String message = "ok";
|
||||||
|
int affected = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<String> statements = splitSql(row.getSqlText());
|
||||||
|
if (statements.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException("SQL 为空");
|
||||||
|
}
|
||||||
|
for (String sql : statements)
|
||||||
|
{
|
||||||
|
affected += executeOne(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
status = "1";
|
||||||
|
message = ex.getMessage() == null ? ex.getClass().getSimpleName() : ex.getMessage();
|
||||||
|
if (message.length() > 1900)
|
||||||
|
{
|
||||||
|
message = message.substring(0, 1900);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Date end = new Date();
|
||||||
|
|
||||||
|
SysDataMigrationLog log = new SysDataMigrationLog();
|
||||||
|
log.setMigrationId(migrationId);
|
||||||
|
log.setTriggerType(StringUtils.isEmpty(triggerType) ? "MANUAL" : triggerType);
|
||||||
|
log.setStartTime(start);
|
||||||
|
log.setEndTime(end);
|
||||||
|
log.setStatus(status);
|
||||||
|
log.setAffectedRows(Integer.valueOf(affected));
|
||||||
|
log.setMessage(message);
|
||||||
|
log.setCreateBy(operator);
|
||||||
|
migrationMapper.insertSysDataMigrationLog(log);
|
||||||
|
|
||||||
|
SysDataMigration patch = new SysDataMigration();
|
||||||
|
patch.setMigrationId(migrationId);
|
||||||
|
patch.setLastRunTime(end);
|
||||||
|
patch.setLastRunStatus(status);
|
||||||
|
patch.setLastRunMessage(message);
|
||||||
|
patch.setUpdateBy(operator);
|
||||||
|
migrationMapper.updateSysDataMigration(patch);
|
||||||
|
|
||||||
|
Map<String, Object> result = new HashMap<String, Object>();
|
||||||
|
result.put("status", status);
|
||||||
|
result.put("affectedRows", Integer.valueOf(affected));
|
||||||
|
result.put("message", message);
|
||||||
|
if ("1".equals(status))
|
||||||
|
{
|
||||||
|
throw new ServiceException(message);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSql(String sqlText)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(sqlText) || StringUtils.isEmpty(sqlText.trim()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("SQL 不能为空");
|
||||||
|
}
|
||||||
|
if (DANGEROUS.matcher(sqlText).find())
|
||||||
|
{
|
||||||
|
throw new ServiceException("SQL 包含禁用关键词");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> splitSql(String sqlText)
|
||||||
|
{
|
||||||
|
List<String> list = new ArrayList<String>();
|
||||||
|
StringBuilder cur = new StringBuilder();
|
||||||
|
boolean inSingle = false;
|
||||||
|
boolean inDouble = false;
|
||||||
|
for (int i = 0; i < sqlText.length(); i++)
|
||||||
|
{
|
||||||
|
char c = sqlText.charAt(i);
|
||||||
|
if (c == '\'' && !inDouble)
|
||||||
|
{
|
||||||
|
inSingle = !inSingle;
|
||||||
|
cur.append(c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == '"' && !inSingle)
|
||||||
|
{
|
||||||
|
inDouble = !inDouble;
|
||||||
|
cur.append(c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c == ';' && !inSingle && !inDouble)
|
||||||
|
{
|
||||||
|
String one = cur.toString().trim();
|
||||||
|
if (!one.isEmpty())
|
||||||
|
{
|
||||||
|
list.add(one);
|
||||||
|
}
|
||||||
|
cur.setLength(0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cur.append(c);
|
||||||
|
}
|
||||||
|
String last = cur.toString().trim();
|
||||||
|
if (!last.isEmpty())
|
||||||
|
{
|
||||||
|
list.add(last);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int executeOne(String sql)
|
||||||
|
{
|
||||||
|
return jdbcTemplate.execute((Connection conn) -> {
|
||||||
|
try (Statement st = conn.createStatement())
|
||||||
|
{
|
||||||
|
boolean hasResult = st.execute(sql);
|
||||||
|
if (hasResult)
|
||||||
|
{
|
||||||
|
try (ResultSet rs = st.getResultSet())
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
if (rs != null)
|
||||||
|
{
|
||||||
|
while (rs.next())
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Integer.valueOf(count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int updated = st.getUpdateCount();
|
||||||
|
return Integer.valueOf(updated < 0 ? 0 : updated);
|
||||||
|
}
|
||||||
|
}).intValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.bms.system.mapper.SysDataMigrationMapper">
|
||||||
|
|
||||||
|
<resultMap type="com.bms.system.domain.SysDataMigration" id="SysDataMigrationResult">
|
||||||
|
<id property="migrationId" column="migration_id" />
|
||||||
|
<result property="migrationName" column="migration_name" />
|
||||||
|
<result property="sqlText" column="sql_text" />
|
||||||
|
<result property="cronExpression" column="cron_expression" />
|
||||||
|
<result property="status" column="status" />
|
||||||
|
<result property="jobId" column="job_id" />
|
||||||
|
<result property="lastRunTime" column="last_run_time" />
|
||||||
|
<result property="lastRunStatus" column="last_run_status" />
|
||||||
|
<result property="lastRunMessage" column="last_run_message" />
|
||||||
|
<result property="remark" column="remark" />
|
||||||
|
<result property="createBy" column="create_by" />
|
||||||
|
<result property="createTime" column="create_time" />
|
||||||
|
<result property="updateBy" column="update_by" />
|
||||||
|
<result property="updateTime" column="update_time" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<resultMap type="com.bms.system.domain.SysDataMigrationLog" id="SysDataMigrationLogResult">
|
||||||
|
<id property="logId" column="log_id" />
|
||||||
|
<result property="migrationId" column="migration_id" />
|
||||||
|
<result property="triggerType" column="trigger_type" />
|
||||||
|
<result property="startTime" column="start_time" />
|
||||||
|
<result property="endTime" column="end_time" />
|
||||||
|
<result property="status" column="status" />
|
||||||
|
<result property="affectedRows" column="affected_rows" />
|
||||||
|
<result property="message" column="message" />
|
||||||
|
<result property="createBy" column="create_by" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<sql id="selectVo">
|
||||||
|
select migration_id, migration_name, sql_text, cron_expression, status, job_id,
|
||||||
|
last_run_time, last_run_status, last_run_message, remark,
|
||||||
|
create_by, create_time, update_by, update_time
|
||||||
|
from sys_data_migration
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectSysDataMigrationById" resultMap="SysDataMigrationResult">
|
||||||
|
<include refid="selectVo"/>
|
||||||
|
where migration_id = #{migrationId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectSysDataMigrationList" parameterType="com.bms.system.domain.SysDataMigration" resultMap="SysDataMigrationResult">
|
||||||
|
<include refid="selectVo"/>
|
||||||
|
<where>
|
||||||
|
<if test="migrationName != null and migrationName != ''">
|
||||||
|
and migration_name like concat('%', #{migrationName}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="status != null and status != ''">and status = #{status}</if>
|
||||||
|
</where>
|
||||||
|
order by migration_id desc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insertSysDataMigration" parameterType="com.bms.system.domain.SysDataMigration" useGeneratedKeys="true" keyProperty="migrationId">
|
||||||
|
insert into sys_data_migration (
|
||||||
|
migration_name, sql_text, cron_expression, status, job_id,
|
||||||
|
last_run_time, last_run_status, last_run_message, remark, create_by, create_time
|
||||||
|
) values (
|
||||||
|
#{migrationName}, #{sqlText}, #{cronExpression}, ifnull(#{status}, '1'), #{jobId},
|
||||||
|
#{lastRunTime}, #{lastRunStatus}, #{lastRunMessage}, #{remark}, #{createBy}, sysdate()
|
||||||
|
)
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="updateSysDataMigration" parameterType="com.bms.system.domain.SysDataMigration">
|
||||||
|
update sys_data_migration
|
||||||
|
<set>
|
||||||
|
<if test="migrationName != null and migrationName != ''">migration_name = #{migrationName},</if>
|
||||||
|
<if test="sqlText != null">sql_text = #{sqlText},</if>
|
||||||
|
<if test="cronExpression != null">cron_expression = #{cronExpression},</if>
|
||||||
|
<if test="status != null and status != ''">status = #{status},</if>
|
||||||
|
<if test="jobId != null">job_id = #{jobId},</if>
|
||||||
|
<if test="lastRunTime != null">last_run_time = #{lastRunTime},</if>
|
||||||
|
<if test="lastRunStatus != null">last_run_status = #{lastRunStatus},</if>
|
||||||
|
<if test="lastRunMessage != null">last_run_message = #{lastRunMessage},</if>
|
||||||
|
<if test="remark != null">remark = #{remark},</if>
|
||||||
|
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||||
|
update_time = sysdate()
|
||||||
|
</set>
|
||||||
|
where migration_id = #{migrationId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="deleteSysDataMigrationByIds">
|
||||||
|
delete from sys_data_migration where migration_id in
|
||||||
|
<foreach collection="array" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<update id="clearJobId">
|
||||||
|
update sys_data_migration set job_id = null, update_time = sysdate() where migration_id = #{migrationId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<insert id="insertSysDataMigrationLog" parameterType="com.bms.system.domain.SysDataMigrationLog" useGeneratedKeys="true" keyProperty="logId">
|
||||||
|
insert into sys_data_migration_log (
|
||||||
|
migration_id, trigger_type, start_time, end_time, status, affected_rows, message, create_by
|
||||||
|
) values (
|
||||||
|
#{migrationId}, #{triggerType}, #{startTime}, #{endTime}, #{status},
|
||||||
|
ifnull(#{affectedRows}, 0), #{message}, #{createBy}
|
||||||
|
)
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<select id="selectSysDataMigrationLogList" parameterType="com.bms.system.domain.SysDataMigrationLog" resultMap="SysDataMigrationLogResult">
|
||||||
|
select log_id, migration_id, trigger_type, start_time, end_time, status, affected_rows, message, create_by
|
||||||
|
from sys_data_migration_log
|
||||||
|
<where>
|
||||||
|
<if test="migrationId != null">and migration_id = #{migrationId}</if>
|
||||||
|
<if test="status != null and status != ''">and status = #{status}</if>
|
||||||
|
<if test="triggerType != null and triggerType != ''">and trigger_type = #{triggerType}</if>
|
||||||
|
</where>
|
||||||
|
order by log_id desc
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
-- Backfill prc_order_bill.product_floor_code / product_floor from prc_order_bill_ext.
|
||||||
|
-- Run after order_bill_product_floor_alter.sql.
|
||||||
|
-- When ext has non-blank channel text, copy to bill (truncate to column length).
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
UPDATE prc_order_bill b
|
||||||
|
INNER JOIN prc_order_bill_ext e ON e.bill_id = b.bill_id
|
||||||
|
SET
|
||||||
|
b.product_floor_code = CASE
|
||||||
|
WHEN e.product_floor_code IS NOT NULL AND TRIM(e.product_floor_code) <> ''
|
||||||
|
THEN LEFT(TRIM(e.product_floor_code), 64)
|
||||||
|
ELSE b.product_floor_code
|
||||||
|
END,
|
||||||
|
b.product_floor = CASE
|
||||||
|
WHEN e.product_floor IS NOT NULL AND TRIM(e.product_floor) <> ''
|
||||||
|
THEN LEFT(TRIM(e.product_floor), 200)
|
||||||
|
ELSE b.product_floor
|
||||||
|
END
|
||||||
|
WHERE (e.product_floor_code IS NOT NULL AND TRIM(e.product_floor_code) <> '')
|
||||||
|
OR (e.product_floor IS NOT NULL AND TRIM(e.product_floor) <> '');
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
-- sys_data_migration + menu under tool (parent menu_id=3).
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sys_data_migration (
|
||||||
|
migration_id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||||
|
migration_name varchar(100) NOT NULL COMMENT '脚本名称',
|
||||||
|
sql_text mediumtext NOT NULL COMMENT 'SQL 文本',
|
||||||
|
cron_expression varchar(255) DEFAULT NULL COMMENT 'Quartz Cron 表达式',
|
||||||
|
status char(1) NOT NULL DEFAULT '1' COMMENT '0启用调度 1停用',
|
||||||
|
job_id bigint(20) DEFAULT NULL COMMENT '关联 sys_job.job_id',
|
||||||
|
last_run_time datetime DEFAULT NULL COMMENT '最近执行时间',
|
||||||
|
last_run_status char(1) DEFAULT NULL COMMENT '0成功 1失败',
|
||||||
|
last_run_message varchar(2000) DEFAULT NULL COMMENT '最近执行信息',
|
||||||
|
remark varchar(500) DEFAULT NULL,
|
||||||
|
create_by varchar(64) DEFAULT NULL,
|
||||||
|
create_time datetime DEFAULT NULL,
|
||||||
|
update_by varchar(64) DEFAULT NULL,
|
||||||
|
update_time datetime DEFAULT NULL,
|
||||||
|
PRIMARY KEY (migration_id),
|
||||||
|
KEY idx_data_migration_status (status),
|
||||||
|
KEY idx_data_migration_job (job_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据迁移脚本';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sys_data_migration_log (
|
||||||
|
log_id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||||
|
migration_id bigint(20) NOT NULL,
|
||||||
|
trigger_type varchar(20) NOT NULL COMMENT 'MANUAL/手动 CRON/定时',
|
||||||
|
start_time datetime DEFAULT NULL,
|
||||||
|
end_time datetime DEFAULT NULL,
|
||||||
|
status char(1) DEFAULT NULL COMMENT '0成功 1失败',
|
||||||
|
affected_rows int(11) DEFAULT 0,
|
||||||
|
message varchar(2000) DEFAULT NULL,
|
||||||
|
create_by varchar(64) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (log_id),
|
||||||
|
KEY idx_data_migration_log_mig (migration_id, start_time)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据迁移执行日志';
|
||||||
|
|
||||||
|
SET @toolParent := (
|
||||||
|
SELECT menu_id FROM sys_menu
|
||||||
|
WHERE menu_type = 'M' AND (menu_id = 3 OR path = 'tool')
|
||||||
|
ORDER BY CASE WHEN menu_id = 3 THEN 0 ELSE 1 END, menu_id
|
||||||
|
LIMIT 1
|
||||||
|
);
|
||||||
|
SET @toolParent := IFNULL(@toolParent, 3);
|
||||||
|
|
||||||
|
INSERT INTO sys_menu (
|
||||||
|
menu_id, menu_name, parent_id, order_num, path, component, query, route_name,
|
||||||
|
is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark
|
||||||
|
)
|
||||||
|
SELECT 2490, N'数据迁移', @toolParent, 10,
|
||||||
|
'migration', 'tool/migration/index', NULL, 'DataMigration',
|
||||||
|
1, 0, 'C', '0', '0', 'tool:migration:list', 'example', 'admin', sysdate(), N'保存 SQL 并配置 Quartz 定时执行'
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2490 OR perms = 'tool:migration:list');
|
||||||
|
|
||||||
|
UPDATE sys_menu
|
||||||
|
SET parent_id = @toolParent, order_num = 10, path = 'migration',
|
||||||
|
component = 'tool/migration/index', visible = '0', status = '0',
|
||||||
|
perms = 'tool:migration:list', menu_type = 'C', icon = 'example'
|
||||||
|
WHERE menu_id = 2490;
|
||||||
|
|
||||||
|
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||||
|
SELECT 2491, N'查询', 2490, 1, '#', '', 1, 0, 'F', '0', '0', 'tool:migration:query', '#', 'admin', sysdate()
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2491);
|
||||||
|
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||||
|
SELECT 2492, N'新增', 2490, 2, '#', '', 1, 0, 'F', '0', '0', 'tool:migration:add', '#', 'admin', sysdate()
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2492);
|
||||||
|
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||||
|
SELECT 2493, N'修改', 2490, 3, '#', '', 1, 0, 'F', '0', '0', 'tool:migration:edit', '#', 'admin', sysdate()
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2493);
|
||||||
|
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||||
|
SELECT 2494, N'删除', 2490, 4, '#', '', 1, 0, 'F', '0', '0', 'tool:migration:remove', '#', 'admin', sysdate()
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2494);
|
||||||
|
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||||
|
SELECT 2495, N'立即执行', 2490, 5, '#', '', 1, 0, 'F', '0', '0', 'tool:migration:run', '#', 'admin', sysdate()
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2495);
|
||||||
|
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
|
||||||
|
SELECT 2496, N'执行日志', 2490, 6, '#', '', 1, 0, 'F', '0', '0', 'tool:migration:log', '#', 'admin', sysdate()
|
||||||
|
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2496);
|
||||||
|
|
||||||
|
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||||
|
SELECT 1, m.menu_id
|
||||||
|
FROM (SELECT 2490 AS menu_id UNION SELECT 2491 UNION SELECT 2492 UNION SELECT 2493
|
||||||
|
UNION SELECT 2494 UNION SELECT 2495 UNION SELECT 2496) m
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id);
|
||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -436,9 +436,29 @@ const exportFieldRef = ref()
|
|||||||
const tableHeight = ref(480)
|
const tableHeight = ref(480)
|
||||||
let tableResizeObserver = null
|
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 = {
|
const defaultParams = {
|
||||||
orderStatuses: [3, 4, 5, 6, 7, 8, 11],
|
orderStatuses: [3, 4, 5, 6, 7, 8, 11],
|
||||||
billingFailStatuses: []
|
billingFailStatuses: [],
|
||||||
|
pickupDateRange: defaultPickupDateRange()
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderStatusMap = {
|
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