MES 实施计划从新建到自动流转:三条入口、一套存储、两类驱动

MES 实施计划从新建到自动流转:三条入口、一套存储、两类驱动

在制造业中,MES(制造执行系统)的实施计划是一个至关重要的环节。本文将详细介绍如何通过三个创建和更新路径来实现MES中的实施计划管理,并确保数据能够从外部同步并自动流转。我们将覆盖数据模型定义、状态码设计以及具体的业务逻辑处理。

数据与概念澄清

MES 计划任务与外部实施计划的区分

在MES系统中,存在两种类型的“计划”:

  • MES 计划任务:由内部生成且不携带外部ID。这种类型的任务可以随时删除。
  • 外部实施计划:来自外部系统的同步数据,具有特定的external_id字段,并且不允许直接从内部删除以保持与外部主数据的一致性。

两者在同一个持久化模型中存储,并通过标记字段来区分其来源和状态。列表查询时通常使用planType参数来过滤这两种类型的数据(例如:仅MES计划、仅带外部ID的实施计划或全部)。

数据建模和状态管理

关键数据表结构

在系统设计过程中,定义了两个关键表:

  • es_plan:存储计划的基本信息如标题、时间范围、状态码等。
  • es_plan_executor:维护计划与执行人之间的多对多关系,并可能触发后续通知。

这些表通过特定字段和关联规则实现了数据的一致性和完整性。以下是部分状态枚举的示例代码:

public enum PlanState {
    CREATED(0, "已创建"),
    HANDLING(1, "进行中"),
    DONE(2, "已结束"),
    CANCELLED(-1, "已取消");

    private int code;
    private String description;

    PlanState(int code, String desc) {
        this.code = code;
        this.description = desc;
    }

    public int getCode() { return code; }
}

实施计划数据创建和更新路径

MES系统提供了多种途径来创建或更新实施计划,这些途径包括后台API、外部同步接口以及合同管理模块。每种方式都有其特定的业务逻辑处理步骤。

路径一:后台 POST /plan/save (强角色)

此接口要求用户具有一定的角色权限,并且在创建新计划时会自动设置默认组织ID:

@PostMapping("/save")
@PreAuthorize("hasRole('sys')")
public OperationInfo
<Object> savePlan(@RequestBody @Validated Plan plan) {
    if (Objects.isNull(plan.getCompanyId())) {
        plan.setCompanyId(DEFAULT_COMPANY_ID);
    }
    return planService.save(plan);
}

路径二:外部实施计划创建接口

该路径用于处理来自第三方系统的同步请求,包括验证SSRWID和accId,并根据这些信息唯一确定计划:

@PostMapping("/create")
@ApiOperation("创建CCC实施计划")
public OperationInfo
<Object> createCccImplementationPlan(@RequestBody CccPlanMappingDTO dto) {
    // 校验输入参数并构建新的 Plan 对象,调用 planService.save 方法保存。
}

路径三:合同同步模块

在涉及合同头的同步过程中,每当外部数据发生变化时,系统会自动更新本地实施计划信息:

// 合同同步接口内部逻辑实现
Integer managerUserId = resolvePlatformUserIdByEmployeeName(cccPlan.getXMJLNAME());
ServiceAccItem localItem = serviceAccItemDao.findByAccId(accId);
if (localContract != null && StringUtils.isNotBlank(localContract.getContractNum())) {
    // 更新合同项的相关信息并保存
}

总结

本文详细介绍了MES系统中实施计划的管理机制,包括数据建模、状态定义以及多种业务场景下的创建和更新流程。通过这些设计与实现,可以有效地确保MES与外部系统的同步一致性,并支持内部对计划任务的操作需求。

后续章节将深入探讨执行人分配逻辑以及其他补充功能的设计细节。希望本文内容能够为MES系统集成开发提供有价值的参考。

4. 数据同步中的关键逻辑

在save方法中处理数据更新时,会遇到一个问题:如何确保在外部系统已经修改了计划的状态之后,再次保存时不被覆盖。为此,在更新分支里执行以下操作:

// 清空状态字段, 确保不会覆盖外部系统修改后的状态 
plan.setStatus(null);
plan.setEndRealTime(null);

// 更新时只处理主体数据
planDao.update(plan);

这一步骤确保计划的主体信息(如标题、负责人等)在保存时,那些定时任务触发后更新的状态字段不被覆盖。另外,在新建分支中,createdBy 的默认值是当前登录用户;然而,如果传入的计划对象包含特定外部标识和最后修改时间,则可以允许使用该计划中的创建人:

Integer createdBy = userId; // 默认当前登录用户为创建者
if (plan.getCreatedBy() != null && StringUtils.isNotBlank(plan.getExternalId()) 
        && plan.getExternalLastModifyTime() != null) {
    createdBy = plan.getCreatedBy();  // 允许外部同步逻辑指定创建人
}
plan.setCreatedBy(createdBy);
planDao.insert(plan);  

这样,实现了在特定场景下覆盖默认行为的能力。

5. 显式状态机设计

显式的状态迁移表使用静态初始化块来定义。PlanState 和 PlanAction 枚举定义了所有可能的状态和动作;而 PlanStateMachine 类则负责根据当前状态和给定的动作决定下一步状态:

static {
    stateList.add(PlanStateDefine.builder()
        .currentState(PlanState.CREATED).action(PlanAction.START)
        .nextState(PlanState.HANDLING).build());
    // 其余迁移规则...
}

这样的设计可以确保所有可能的状态变更都被明确定义并且是可维护的。例如,当计划到达预定结束时间时, 系统会调用 getNextState 方法以确定下一步状态,并更新数据库。

public OperationInfo
<Object> endPlan(Plan plan) {
    Integer planId = plan.getId();
    if (planId == null) {
        return OperationInfo.failure(SpringUtil.getMessage(I18nMessageKey.PARAM_ERROR));
    }

    Plan existingPlan = planDao.findById(planId);
    if (existingPlan == null) {
        return OperationInfo.failure();
    }

    Integer nextState = PlanStateMachine.getNextState(existingPlan.getStatus(), PlanAction.END);
    Plan newPlan = new Plan();
    newPlan.setId(plan.getId());
    newPlan.setStatus(nextState);       // 更新状态
    newPlan.setScore(plan.getScore());  // 其他属性更新
    newPlan.setFeedback(plan.getFeedback());

    planDao.update(newPlan);

    return OperationInfo.success();
}

这种方法能够确保所有状态转换都是受控的,并避免了非法的状态迁移。

6. 自动流转流程设计

为实现计划任务的自动流转,引入定时任务来定期检查数据库中需要状态更新的任务。使用 Redisson 实现分布式锁防止同一个任务被重复处理:

@Scheduled(cron = "0 */1 * * * *")
public void startPlanTaskLoad() {
    RLock lock = redissonClient.getLock("MES_PLAN_TASK_LOAD_START_LOCK");

    if (lock.tryLock(500, TimeUnit.MILLISECONDS)) { // 获取锁,最多等待 500ms
        try {
            List
<Plan> plansToStart = planDao.findNeedStartPlanList();

            for (Plan plan : plansToStart) { 
                Integer nextState = PlanStateMachine.getNextState(plan.getStatus(), PlanAction.START);
                planDao.updateStatus(plan.getId(), nextState); // 更新状态为 HANDLING
            }

            // 对于逾期任务,同样进行状态更新
        } finally {
            lock.unlock();  // 确保释放锁
        }
    }
}

这些定时任务依据预先定义好的查询语句从数据库中获取需要流转的任务列表,并根据当前的状态和动作转到下一个合适的状态。此外,还需要为每个具体的计划类型定义相应的查询条件:

<select id="findNeedStartPlanList" resultType="com.enmo.enmo_support.workbench.model.Plan">
    -- 查询所有状态为创建且开始日期在今天的任务 --
</select>

这保证了系统的稳定性和一致性。

7. 安全删除机制

为了避免误删重要数据,系统限制用户不能直接删除具有external_id的实施计划条目:

if (StringUtils.isNotBlank(plan.getExternalId())) {
    return OperationInfo.failure("外部计划不允许被删除");
}

这样的检查确保了只有那些没有外部标识符的数据才可以进行物理删除。

8. 动态更新执行人及通知机制

当修改任务的当前执行者时,需要特别处理旧执行者的移除和新执行者的添加:

// 更新执行人的逻辑通常涉及到:
if (oldExecutorId != newExecutorId) {
    // 移除旧执行者并重新分配给新的执行者,同时发送通知
} else {
    // 只更新其他字段如备注或工时而不触发通知
}

这能确保新旧责任人之间的顺利交接,并且只在必要的情况下才进行额外的通知操作。

9. 系统设计与扩展性

为了维护系统的可读性和方便后续的升级,建议按照以下步骤来理解并修改系统:

  1. 状态枚举及迁移:熟悉PlanState 和 PlanAction 的定义以及它们如何影响迁移规则。
  2. 查找和更新查询:了解用于初始化任务的状态查询。
  3. 定时任务逻辑:确保分布式锁的正确使用,并关注updateStatus方法的具体实现。
  4. 保存与结束逻辑:检查计划保存、结束等核心逻辑的方法以识别潜在的问题点。
  5. 外部接口:熟悉如何从外部系统接收数据及同步信息。

通过这种方式,可以有效地管理和扩展状态机机制。

10. 系统运维和故障排除指南

在维护该系统时,请注意以下几点:

  • 检查是否所有用户角色都正确配置了访问不同类型接口的权限。
  • 跨环境问题应首先检查数据库与服务器之间的时间同步设置,确保时间的一致性。
  • 对于多实例部署,确保分布式锁服务可以正常运行以避免任务重复执行或遗漏。