diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/tool/DataMigrationController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/tool/DataMigrationController.java new file mode 100644 index 00000000..23efeeb6 --- /dev/null +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/tool/DataMigrationController.java @@ -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 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 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 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); + } +} diff --git a/bms-back/bms-common/src/main/java/com/bms/common/constant/Constants.java b/bms-back/bms-common/src/main/java/com/bms/common/constant/Constants.java index f966f8b6..3d8b1ea2 100644 --- a/bms-back/bms-common/src/main/java/com/bms/common/constant/Constants.java +++ b/bms-back/bms-common/src/main/java/com/bms/common/constant/Constants.java @@ -164,10 +164,11 @@ public class Constants * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加) * com.bms.customer 覆盖客户/供应商资料同步、客户授信到期等业务任务包; * 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", - "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" }; /** * 定时任务违规的字符 diff --git a/bms-back/bms-system/src/main/java/com/bms/system/domain/SysDataMigration.java b/bms-back/bms-system/src/main/java/com/bms/system/domain/SysDataMigration.java new file mode 100644 index 00000000..d4bafdc9 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/system/domain/SysDataMigration.java @@ -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; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/system/domain/SysDataMigrationLog.java b/bms-back/bms-system/src/main/java/com/bms/system/domain/SysDataMigrationLog.java new file mode 100644 index 00000000..80eefc92 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/system/domain/SysDataMigrationLog.java @@ -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; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/system/mapper/SysDataMigrationMapper.java b/bms-back/bms-system/src/main/java/com/bms/system/mapper/SysDataMigrationMapper.java new file mode 100644 index 00000000..ff24e014 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/system/mapper/SysDataMigrationMapper.java @@ -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 selectSysDataMigrationList(SysDataMigration query); + + int insertSysDataMigration(SysDataMigration row); + + int updateSysDataMigration(SysDataMigration row); + + int deleteSysDataMigrationByIds(Long[] migrationIds); + + int clearJobId(Long migrationId); + + int insertSysDataMigrationLog(SysDataMigrationLog row); + + List selectSysDataMigrationLogList(SysDataMigrationLog query); +} diff --git a/bms-back/bms-system/src/main/java/com/bms/system/migration/task/DataMigrationTask.java b/bms-back/bms-system/src/main/java/com/bms/system/migration/task/DataMigrationTask.java new file mode 100644 index 00000000..45015886 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/system/migration/task/DataMigrationTask.java @@ -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"); + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/system/service/ISysDataMigrationService.java b/bms-back/bms-system/src/main/java/com/bms/system/service/ISysDataMigrationService.java new file mode 100644 index 00000000..9ddf689b --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/system/service/ISysDataMigrationService.java @@ -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 selectSysDataMigrationList(SysDataMigration query); + + int insertSysDataMigration(SysDataMigration row); + + int updateSysDataMigration(SysDataMigration row); + + int deleteSysDataMigrationByIds(Long[] migrationIds); + + int clearJobId(Long migrationId); + + List selectSysDataMigrationLogList(SysDataMigrationLog query); + + /** + * Run saved SQL. triggerType: MANUAL / CRON. + * @return summary map: status, affectedRows, message + */ + Map executeMigration(Long migrationId, String triggerType, String operator); +} diff --git a/bms-back/bms-system/src/main/java/com/bms/system/service/impl/SysDataMigrationServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/system/service/impl/SysDataMigrationServiceImpl.java new file mode 100644 index 00000000..1120734d --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/system/service/impl/SysDataMigrationServiceImpl.java @@ -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 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 selectSysDataMigrationLogList(SysDataMigrationLog query) + { + return migrationMapper.selectSysDataMigrationLogList(query); + } + + @Override + public Map 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 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 result = new HashMap(); + 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 splitSql(String sqlText) + { + List list = new ArrayList(); + 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(); + } +} diff --git a/bms-back/bms-system/src/main/resources/mapper/system/SysDataMigrationMapper.xml b/bms-back/bms-system/src/main/resources/mapper/system/SysDataMigrationMapper.xml new file mode 100644 index 00000000..7adef64c --- /dev/null +++ b/bms-back/bms-system/src/main/resources/mapper/system/SysDataMigrationMapper.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + + + + 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() + ) + + + + update sys_data_migration + + migration_name = #{migrationName}, + sql_text = #{sqlText}, + cron_expression = #{cronExpression}, + status = #{status}, + job_id = #{jobId}, + last_run_time = #{lastRunTime}, + last_run_status = #{lastRunStatus}, + last_run_message = #{lastRunMessage}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where migration_id = #{migrationId} + + + + delete from sys_data_migration where migration_id in + #{id} + + + + update sys_data_migration set job_id = null, update_time = sysdate() where migration_id = #{migrationId} + + + + 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} + ) + + + + diff --git a/bms-back/sql/price/order_bill_product_floor_backfill.sql b/bms-back/sql/price/order_bill_product_floor_backfill.sql new file mode 100644 index 00000000..29d94957 --- /dev/null +++ b/bms-back/sql/price/order_bill_product_floor_backfill.sql @@ -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) <> ''); diff --git a/bms-back/sql/system/sys_data_migration.sql b/bms-back/sql/system/sys_data_migration.sql new file mode 100644 index 00000000..9e30e16f --- /dev/null +++ b/bms-back/sql/system/sys_data_migration.sql @@ -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); diff --git a/bms-front/src/api/tool/migration.js b/bms-front/src/api/tool/migration.js new file mode 100644 index 00000000..fbea2bd8 --- /dev/null +++ b/bms-front/src/api/tool/migration.js @@ -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 + }) +} diff --git a/bms-front/src/views/price/billing/bill.vue b/bms-front/src/views/price/billing/bill.vue index d7cb1363..f45bc889 100644 --- a/bms-front/src/views/price/billing/bill.vue +++ b/bms-front/src/views/price/billing/bill.vue @@ -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 = { diff --git a/bms-front/src/views/tool/migration/index.vue b/bms-front/src/views/tool/migration/index.vue new file mode 100644 index 00000000..e6693570 --- /dev/null +++ b/bms-front/src/views/tool/migration/index.vue @@ -0,0 +1,378 @@ + + +