From 8d6458cdea997047411e68a5d8c31f5453358c59 Mon Sep 17 00:00:00 2001 From: zhangzq Date: Tue, 11 Aug 2026 10:47:41 +0800 Subject: [PATCH] init --- .../entity/PlanExceptionLog.java | 46 +++ .../job/PlanExceptionCollectJob.java | 20 ++ .../mapper/PlanExceptionLogMapper.java | 7 + .../service/PlanExceptionCollectService.java | 5 + .../impl/PlanExceptionCollectServiceImpl.java | 142 ++++++++ .../service/quartz/config/JobRunner.java | 3 + pms/src/main/resources/plan_exception_log.sql | 42 +++ pms_vue/src/router/index.js | 334 +++--------------- pms_vue/src/views/project/index.vue | 2 +- pms_vue/src/views/project/planIndex.vue | 2 +- 10 files changed, 307 insertions(+), 296 deletions(-) create mode 100644 pms/src/main/java/org/nl/pms/planexception/entity/PlanExceptionLog.java create mode 100644 pms/src/main/java/org/nl/pms/planexception/job/PlanExceptionCollectJob.java create mode 100644 pms/src/main/java/org/nl/pms/planexception/mapper/PlanExceptionLogMapper.java create mode 100644 pms/src/main/java/org/nl/pms/planexception/service/PlanExceptionCollectService.java create mode 100644 pms/src/main/java/org/nl/pms/planexception/service/impl/PlanExceptionCollectServiceImpl.java create mode 100644 pms/src/main/resources/plan_exception_log.sql diff --git a/pms/src/main/java/org/nl/pms/planexception/entity/PlanExceptionLog.java b/pms/src/main/java/org/nl/pms/planexception/entity/PlanExceptionLog.java new file mode 100644 index 0000000..bcd1595 --- /dev/null +++ b/pms/src/main/java/org/nl/pms/planexception/entity/PlanExceptionLog.java @@ -0,0 +1,46 @@ +package org.nl.pms.planexception.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("plan_exception_log") +public class PlanExceptionLog { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @TableField("project_id") + private String projectId; + + @TableField("plan_detel_id") + private String planDetelId; + + @TableField("exception_type") + private Integer exceptionType; + + @TableField("found_time") + private LocalDateTime foundTime; + + @TableField("delay_duration") + private String delayDuration; + + @TableField("impact_scope") + private Integer impactScope; + + private String description; + + @TableField("root_cause") + private String rootCause; + + @TableField("created_name") + private String createdName; + + @TableField("created_time") + private LocalDateTime createdTime; +} diff --git a/pms/src/main/java/org/nl/pms/planexception/job/PlanExceptionCollectJob.java b/pms/src/main/java/org/nl/pms/planexception/job/PlanExceptionCollectJob.java new file mode 100644 index 0000000..dbaaae0 --- /dev/null +++ b/pms/src/main/java/org/nl/pms/planexception/job/PlanExceptionCollectJob.java @@ -0,0 +1,20 @@ +package org.nl.pms.planexception.job; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.nl.pms.planexception.service.PlanExceptionCollectService; +import org.springframework.stereotype.Component; + +@Slf4j +@Component("planExceptionCollectJob") +@RequiredArgsConstructor +public class PlanExceptionCollectJob { + + private final PlanExceptionCollectService planExceptionCollectService; + + public void collect() { + log.info("开始执行计划异常工作项收集任务"); + planExceptionCollectService.collect(); + log.info("计划异常工作项收集任务执行完成"); + } +} diff --git a/pms/src/main/java/org/nl/pms/planexception/mapper/PlanExceptionLogMapper.java b/pms/src/main/java/org/nl/pms/planexception/mapper/PlanExceptionLogMapper.java new file mode 100644 index 0000000..1c029b6 --- /dev/null +++ b/pms/src/main/java/org/nl/pms/planexception/mapper/PlanExceptionLogMapper.java @@ -0,0 +1,7 @@ +package org.nl.pms.planexception.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.nl.pms.planexception.entity.PlanExceptionLog; + +public interface PlanExceptionLogMapper extends BaseMapper { +} diff --git a/pms/src/main/java/org/nl/pms/planexception/service/PlanExceptionCollectService.java b/pms/src/main/java/org/nl/pms/planexception/service/PlanExceptionCollectService.java new file mode 100644 index 0000000..688f716 --- /dev/null +++ b/pms/src/main/java/org/nl/pms/planexception/service/PlanExceptionCollectService.java @@ -0,0 +1,5 @@ +package org.nl.pms.planexception.service; + +public interface PlanExceptionCollectService { + void collect(); +} diff --git a/pms/src/main/java/org/nl/pms/planexception/service/impl/PlanExceptionCollectServiceImpl.java b/pms/src/main/java/org/nl/pms/planexception/service/impl/PlanExceptionCollectServiceImpl.java new file mode 100644 index 0000000..9fb2e28 --- /dev/null +++ b/pms/src/main/java/org/nl/pms/planexception/service/impl/PlanExceptionCollectServiceImpl.java @@ -0,0 +1,142 @@ +package org.nl.pms.planexception.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.nl.pms.planexception.entity.PlanExceptionLog; +import org.nl.pms.planexception.mapper.PlanExceptionLogMapper; +import org.nl.pms.planexception.service.PlanExceptionCollectService; +import org.nl.pms.project.entity.Project; +import org.nl.pms.project.mapper.ProjectMapper; +import org.nl.pms.projectPlan.entity.ProjectPlanDetail; +import org.nl.pms.projectPlan.mapper.ProjectPlanDetailMapper; +import org.nl.pms.projectProgressReport.entity.ProjectProgressReport; +import org.nl.pms.projectProgressReport.mapper.ProjectProgressReportMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PlanExceptionCollectServiceImpl implements PlanExceptionCollectService { + + private static final int EXCEPTION_DELAYED_REPORT = 1; + private static final int EXCEPTION_DELAYED_TASK = 2; + private static final int REPORT_CONFIRMED = 2; + private static final int TASK_COMPLETED = 3; + private static final String SYSTEM_CREATOR = "系统定时任务"; + + private final ProjectMapper projectMapper; + private final ProjectPlanDetailMapper detailMapper; + private final ProjectProgressReportMapper reportMapper; + private final PlanExceptionLogMapper exceptionLogMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public void collect() { + LocalDate today = LocalDate.now(); + List details = detailMapper.selectList(new LambdaQueryWrapper() + .and(wrapper -> wrapper.ne(ProjectPlanDetail::getStatus, TASK_COMPLETED) + .or().isNull(ProjectPlanDetail::getStatus))); + if (details.isEmpty()) { + return; + } + Map projects = projectMapper.selectBatchIds(details.stream() + .map(ProjectPlanDetail::getProjectId).distinct().toList()) + .stream().collect(Collectors.toMap(Project::getId, project -> project)); + Map latestConfirmedReportDates = latestConfirmedReportDates(details); + for (ProjectPlanDetail detail : details) { + Project project = projects.get(detail.getProjectId()); + if (project == null) { + continue; + } + if (isTaskOverdue(detail, today)) { + saveDelayedTask(detail, today); + continue; + } + if (isSupportedReportRate(project.getReportRate())) { + saveDelayedReportWhenNeeded(detail, project, latestConfirmedReportDates.get(detail.getId()), today); + } + } + } + + private Map latestConfirmedReportDates(List details) { + List detailIds = details.stream().map(ProjectPlanDetail::getId).toList(); + if (detailIds.isEmpty()) { + return Map.of(); + } + return reportMapper.selectList(new LambdaQueryWrapper() + .in(ProjectProgressReport::getDetailId, detailIds) + .eq(ProjectProgressReport::getStatus, REPORT_CONFIRMED) + .isNotNull(ProjectProgressReport::getReportDate)) + .stream().collect(Collectors.toMap(ProjectProgressReport::getDetailId, ProjectProgressReport::getReportDate, + (left, right) -> left.isAfter(right) ? left : right)); + } + + private boolean isTaskOverdue(ProjectPlanDetail detail, LocalDate today) { + return detail.getDevEndDate() != null && detail.getDevEndDate().isBefore(today); + } + + private void saveDelayedTask(ProjectPlanDetail detail, LocalDate today) { + long overdueDays = ChronoUnit.DAYS.between(detail.getDevEndDate(), today); + String description = String.format("计划明细“%s”未完成,计划结束日期为 %s,当前已延期 %d 天。", + detail.getDetailName(), detail.getDevEndDate(), overdueDays); + save(detail, EXCEPTION_DELAYED_TASK, overdueDays, description, + "计划任务未在计划结束日期前完成"); + } + + private void saveDelayedReportWhenNeeded(ProjectPlanDetail detail, Project project, LocalDate lastReportDate, LocalDate today) { + if (detail.getDevStartDate() == null) { + return; + } + LocalDate calculationBaseDate = lastReportDate == null ? detail.getDevStartDate() : lastReportDate; + LocalDate dueReportDate = calculationBaseDate.plusDays(project.getReportRate()); + if (!today.isAfter(dueReportDate)) { + return; + } + long overdueDays = ChronoUnit.DAYS.between(dueReportDate, today); + String reportBaseText = lastReportDate == null ? "暂无已确认报工,按计划开始日期" : "上次已确认报工日期"; + String description = String.format("计划明细“%s”%s %s 计算,应报日期为 %s,当前已逾期 %d 天未报工。", + detail.getDetailName(), reportBaseText, calculationBaseDate, dueReportDate, overdueDays); + save(detail, EXCEPTION_DELAYED_REPORT, overdueDays, description, + "超过项目规定报工频率未提交已确认报工"); + } + + private void save(ProjectPlanDetail detail, int exceptionType, long overdueDays, + String description, String rootCause) { + PlanExceptionLog record = new PlanExceptionLog(); + record.setProjectId(String.valueOf(detail.getProjectId())); + record.setPlanDetelId(String.valueOf(detail.getId())); + record.setExceptionType(exceptionType); + LocalDateTime now = LocalDateTime.now(); + record.setFoundTime(now); + record.setDelayDuration(overdueDays + "d"); + record.setImpactScope(impactScope(overdueDays)); + record.setDescription(description); + record.setRootCause(rootCause); + record.setCreatedName(SYSTEM_CREATOR); + record.setCreatedTime(LocalDateTime.now()); + exceptionLogMapper.insert(record); + log.info("记录计划异常:detailId={}, exceptionType={}, overdueDays={}", detail.getId(), exceptionType, overdueDays); + } + + private boolean isSupportedReportRate(Integer reportRate) { + return Integer.valueOf(1).equals(reportRate) + || Integer.valueOf(7).equals(reportRate) + || Integer.valueOf(15).equals(reportRate); + } + + private int impactScope(long overdueDays) { + if (overdueDays <= 1) { + return 1; + } + return overdueDays <= 3 ? 2 : 3; + } +} diff --git a/pms/src/main/java/org/nl/pms/system/service/quartz/config/JobRunner.java b/pms/src/main/java/org/nl/pms/system/service/quartz/config/JobRunner.java index 5d8b199..32bb7e4 100644 --- a/pms/src/main/java/org/nl/pms/system/service/quartz/config/JobRunner.java +++ b/pms/src/main/java/org/nl/pms/system/service/quartz/config/JobRunner.java @@ -33,5 +33,8 @@ public class JobRunner implements ApplicationRunner { */ @Override public void run(ApplicationArguments applicationArguments) { + java.util.List activeJobs = quartzJobService.findByIsPauseIsFalse(); + activeJobs.forEach(quartzManage::addJob); + log.info("已恢复 {} 个启用的定时任务", activeJobs.size()); } } diff --git a/pms/src/main/resources/plan_exception_log.sql b/pms/src/main/resources/plan_exception_log.sql new file mode 100644 index 0000000..fd81c05 --- /dev/null +++ b/pms/src/main/resources/plan_exception_log.sql @@ -0,0 +1,42 @@ +-- 计划异常工作项记录表 +CREATE TABLE IF NOT EXISTS plan_exception_log ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键', + project_id VARCHAR(50) NOT NULL COMMENT '项目ID', + plan_detel_id VARCHAR(50) NOT NULL COMMENT '计划ID (关联WBS或里程碑)', + exception_type TINYINT NOT NULL COMMENT '异常类型: 1-延期报工, 2-延期任务, 3-重新排期', + found_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '异常发现时间', + delay_duration VARCHAR(20) DEFAULT NULL COMMENT '延期时间 (如: 4h, 2d),类型为3时置NULL', + impact_scope TINYINT NOT NULL COMMENT '影响范围: 1-小, 2-中, 3-大', + description TEXT NOT NULL COMMENT '详细说明', + root_cause TEXT DEFAULT NULL COMMENT '根本原因', + created_name VARCHAR(50) NOT NULL COMMENT '创建人', + created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', + PRIMARY KEY (id), + KEY idx_exception_project_detail (project_id, plan_detel_id), + KEY idx_exception_found_time (found_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='人员异常工作项记录表'; + +-- 每天 23:00 收集未完成计划的延期任务和延期报工异常。 +-- Quartz 通过 bean_name + method_name 反射调用该 Spring Bean 的无参方法。 +INSERT INTO sys_quartz_job ( + job_id, bean_name, cron_expression, is_pause, job_name, method_name, params, + description, person_in_charge, email, sub_task, pause_after_failure, + create_id, create_name, create_time, update_id, update_name, update_time +) VALUES ( + 'plan_exception_collect', 'planExceptionCollectJob', '0 0 23 * * ?', 0, + '计划异常工作项收集', 'collect', NULL, + '每日收集延期任务和延期报工异常记录', '系统', NULL, NULL, 0, + 'system', '系统', NOW(), 'system', '系统', NOW() +) +ON DUPLICATE KEY UPDATE + bean_name = VALUES(bean_name), + cron_expression = VALUES(cron_expression), + is_pause = VALUES(is_pause), + job_name = VALUES(job_name), + method_name = VALUES(method_name), + params = VALUES(params), + description = VALUES(description), + pause_after_failure = VALUES(pause_after_failure), + update_id = VALUES(update_id), + update_name = VALUES(update_name), + update_time = NOW(); diff --git a/pms_vue/src/router/index.js b/pms_vue/src/router/index.js index 35d5339..610241d 100644 --- a/pms_vue/src/router/index.js +++ b/pms_vue/src/router/index.js @@ -1,319 +1,65 @@ import router from './routers' import store from '@/store' -import NProgress from 'nprogress' // progress bar -import 'nprogress/nprogress.css' // progress bar style -import { getToken } from '@/utils/auth' // getToken from cookie +import NProgress from 'nprogress' +import 'nprogress/nprogress.css' +import { getToken } from '@/utils/auth' import { filterAsyncRouter } from '@/store/modules/permission' +import { buildMenus } from '@/views/system/menu/menu' -const staticMenuData = [ - { - name: '项目管理', - path: '/pms', - hidden: false, - redirect: 'noredirect', - component: 'Layout', - alwaysShow: true, - meta: { - title: '项目管理', - icon: 'system', - noCache: true - }, - children: [ - { - name: 'Project', - path: 'project', - hidden: false, - component: 'project/index', - meta: { - title: '项目管理', - icon: 'clipboard', - noCache: true - } - }, - { - name: 'ProjectPlan', - path: 'project-plan', - hidden: false, - component: 'project/planIndex', - meta: { - title: '项目开发计划表', - icon: 'clipboard', - noCache: true - } - }, - { - name: 'ProjectDelayRecord', - path: 'project-delay-records', - hidden: false, - component: 'project/delayRecordIndex', - meta: { - title: '项目延期记录管理', - icon: 'clipboard', - noCache: true - } - }, - { - name: 'TemporaryWork', - path: 'temporary-work', - hidden: false, - component: 'project/temporaryWorkIndex', - meta: { - title: '我的临时工作管理', - icon: 'clipboard', - noCache: true - } - }, - { - name: 'ProgressReport', - path: 'progress-report', - hidden: false, - component: 'project/progressReportIndex', - meta: { - title: '报工作业', - icon: 'clipboard', - noCache: true - } - } - ] - }, - { - name: '审批管理', - path: '/approval', - hidden: false, - redirect: 'noredirect', - component: 'Layout', - alwaysShow: true, - meta: { - title: '审批管理', - icon: 'system', - noCache: true - }, - children: [ - { - name: 'ProgressReportAudit', - path: 'progress-report-audit', - hidden: false, - component: 'project/progressReportAuditIndex', - meta: { - title: '报工审核', - icon: 'clipboard', - noCache: true - } - } - ] - }, - { - name: '系统管理', - path: '/system', - hidden: false, - redirect: 'noredirect', - component: 'Layout', - alwaysShow: true, - meta: { - title: '系统管理', - icon: 'system', - noCache: true - }, - children: [ - { - name: 'User', - path: 'user', - hidden: false, - component: 'system/user/index', - meta: { - title: '用户管理', - icon: 'peoples', - noCache: true - } - }, - { - name: 'Role', - path: 'role', - hidden: false, - component: 'system/role/index', - meta: { - title: '角色管理', - icon: 'role', - noCache: true - } - }, - { - name: 'Menu', - path: 'menu', - hidden: false, - component: 'system/menu/index', - meta: { - title: '菜单管理', - icon: 'menu', - noCache: true - } - }, - { - name: 'Dept', - path: 'dept', - hidden: false, - component: 'system/dept/index', - meta: { - title: '部门管理', - icon: 'dept', - noCache: true - } - }, - { - name: 'Dict', - path: 'dict', - hidden: false, - component: 'system/dict/index', - meta: { - title: '字典管理', - icon: 'dictionary', - noCache: true - } - }, - { - name: 'Mock', - path: 'mock', - hidden: false, - component: 'system/mock/index', - meta: { - title: '接口mock', - icon: 'backup', - noCache: true - } - }, - { - name: 'Param', - path: 'param', - hidden: false, - component: 'system/param/index', - meta: { - title: '系统参数', - icon: 'Steve-Jobs', - noCache: true - } - }, - { - name: 'DataPermission', - path: 'dataPermission', - hidden: false, - component: 'system/dataPermission/index', - meta: { - title: '数据权限', - icon: 'Steve-Jobs', - noCache: true - } - }, - { - name: 'Timing', - path: 'timing', - hidden: false, - component: 'system/timing/index', - meta: { - title: '任务调度', - icon: 'timing', - noCache: true - } - } - ] - }, - { - name: '系统监控', - path: '/monitor', - hidden: false, - redirect: 'noredirect', - component: 'Layout', - alwaysShow: true, - meta: { - title: '系统监控', - icon: 'monitor', - noCache: true - }, - children: [ - { - name: 'Redis', - path: 'redis', - hidden: false, - component: 'system/redis/index', - meta: { - title: 'Redis监控', - icon: 'radio', - noCache: true - } - }, - { - name: 'SysNotice', - path: 'sysNotice', - hidden: false, - component: 'system/notice/index', - meta: { - title: '通知管理', - icon: 'develop', - noCache: true - } - } - ] - } -] +const SYSTEM_TYPE = 1 -NProgress.configure({ showSpinner: false })// NProgress Configuration +NProgress.configure({ showSpinner: false }) -const whiteList = ['/login']// no redirect whitelist +const whiteList = ['/login'] router.beforeEach((to, from, next) => { if (to.meta.title) { - // document.title = to.meta.title + ' - ' + Config.title document.title = to.meta.title } NProgress.start() - if (getToken()) { - // 已登录且要跳转的页面是登录页 - if (to.path === '/login') { - next({ path: '/' }) - NProgress.done() - } else { - if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息 - store.dispatch('GetInfo').then(() => { // 拉取user_info - // 动态路由,拉取菜单 - loadMenus(next, to) - }).catch(() => { - store.dispatch('LogOut').then(() => { - location.reload() // 为了重新实例化vue-router对象 避免bug - }) - }) - // 登录时未拉取 菜单,在此处拉取 - } else if (store.getters.loadMenus) { - // 修改成false,防止死循环 - store.dispatch('updateLoadMenus') - loadMenus(next, to) - } else { - next() - } - } - } else { - /* has no token*/ - if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入 + if (!getToken()) { + if (whiteList.includes(to.path)) { next() } else { - next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页 + next(`/login?redirect=${to.fullPath}`) NProgress.done() } + return + } + if (to.path === '/login') { + next({ path: '/' }) + NProgress.done() + return + } + if (store.getters.roles.length === 0) { + store.dispatch('GetInfo').then(() => loadMenus(next, to)).catch(() => { + store.dispatch('LogOut').then(() => location.reload()) + }) + } else if (store.getters.loadMenus) { + store.dispatch('updateLoadMenus') + loadMenus(next, to) + } else { + next() } }) export const loadMenus = (next, to) => { - const sdata = JSON.parse(JSON.stringify(staticMenuData)) - const rdata = JSON.parse(JSON.stringify(staticMenuData)) - const sidebarRoutes = filterAsyncRouter(sdata) - const rewriteRoutes = filterAsyncRouter(rdata, false, true) - rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true }) - - store.dispatch('GenerateRoutes', rewriteRoutes).then(() => { - router.addRoutes(rewriteRoutes) - next({ ...to, replace: true }) + buildMenus(SYSTEM_TYPE).then(res => { + const menus = Array.isArray(res) ? res : (res.data || []) + const sidebarRoutes = filterAsyncRouter(JSON.parse(JSON.stringify(menus))) + const rewriteRoutes = filterAsyncRouter(JSON.parse(JSON.stringify(menus)), false, true) + rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true }) + store.dispatch('GenerateRoutes', rewriteRoutes).then(() => { + router.addRoutes(rewriteRoutes) + next({ ...to, replace: true }) + }) + store.dispatch('SetSidebarRouters', sidebarRoutes) + }).catch(() => { + NProgress.done() + next(false) }) - store.dispatch('SetSidebarRouters', sidebarRoutes) } router.afterEach(() => { - NProgress.done() // finish progress bar + NProgress.done() }) diff --git a/pms_vue/src/views/project/index.vue b/pms_vue/src/views/project/index.vue index ea2a316..aad6cab 100644 --- a/pms_vue/src/views/project/index.vue +++ b/pms_vue/src/views/project/index.vue @@ -852,7 +852,7 @@ export default { return match ? match.label : '未知' }, reportRateLabel(rate) { - return ({ 1: '每日', 7: '每周', 30: '每月' })[rate] || '-' + return ({ 1: '每日', 7: '每周', 15: '每半月' })[rate] || '-' }, statusTagType(status) { const typeMap = { diff --git a/pms_vue/src/views/project/planIndex.vue b/pms_vue/src/views/project/planIndex.vue index 53ab5f0..89d9f0b 100644 --- a/pms_vue/src/views/project/planIndex.vue +++ b/pms_vue/src/views/project/planIndex.vue @@ -361,7 +361,7 @@ export default { return plan }, reportRateLabel(rate) { - return ({ 1: '每日', 7: '每周', 30: '每月' })[rate] || '-' + return ({ 1: '每日', 7: '每周', 15: '每半月' })[rate] || '-' }, addMaster() { this.plan.masters.push(this.createMaster())