动态代理 多态使用 bean依赖注入 实际使用场景

远程调用异常处理jdk动态代理

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

package com.huawei.it.external.config;

import com.alibaba.fastjson.JSON;
import com.huawei.it.external.service.ITWoRemoteService;
import com.huawei.it.jalor5.core.exception.ApplicationException;
import com.huawei.it.jalor5.core.log.ILogger;
import com.huawei.it.jalor5.core.log.JalorLoggerFactory;
import com.huawei.it.jalor5.vegahsa.client.rpc.factory.RpcClientFactory;
import com.huawei.it.util.excetion.BizApplicationException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;

import javax.inject.Named;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 功能描述:封装ITWoRemoteService的代理对象,对远程调用的异常情况进行处理
 * 起因:通过Jalor RPC调用,远程调用发生异常时,会将远程异常封装为非ApplicationException
 *      具体封装的异常详见VegaRestTemplate.getErrorHandler().handleError()
 *      而由于Jalor统一对异常进行了封装,详见WebServiceExceptionMapper,此种方式会导致
 *      非ApplicationException异常,code统一为unknown,message统一为标准异常术语,最终
 *      的结果是远程服务抛出的异常提示无法在客户端正常显示
 * 这个类封装了什么:本类对异常各种异常进行了捕获,并对异常提示进行封装
 * (1)对于4xx异常,抛出异常为”远程服务网络不通,或您无权限进行相关操作”
 * (2)对于5xx异常,从code中解析异常信息
 * (WO系统中直接使用throw new BizApplicationException("异常信息"),实际异常信息是封装在code中的)
 * 如果异常信息不为null且不为"",则直接构建BizApplicationException抛出
 * 如果异常信息为null或为"",抛出异常为“服务调用发生异常,请稍后重试”
 * (3)对于其他异常,统一抛出“发生未知异常,请联系相关人员”
 *
 * @author YaoJiang-wx1047757
 * @since 2021-05-08
 */
@Named("tWoRemoteService")
public class ITWoRemoteServiceFactoryBean implements FactoryBean<ITWoRemoteService> {
    private static final ILogger LOGGER = JalorLoggerFactory.getLogger(ITWoRemoteService.class);

    // unknown异常及正常5xx在返回体中的特征值
    private static final String UNKNOWN = "code=unknown";
    private static final String ERRCODE_PREFIX = "code=JALOR_EX$";
    private static final Integer PREFIX_LENGTH = 14;

    // 实际用于执行远程调用的类,是ITWoRemoteService的一个代理类
    private static final ITWoRemoteService REMOTE_TARGET = RpcClientFactory.getSubAppRpcClinet(ITWoRemoteService.class);

    /**
     * 功能描述:重写FactoryBean的getObject()方法
     * 内部对ITWoRemoteService接口创建了代理,代理内部实际上是使用Jalor生成的远程调用代理
     * 执行远程调用,对调用过程中产生的异常进行捕获,将异常拆分为5xx/4xx/其他异常等三种类型
     *
     * @author YaoJiang-wx1047757
     * @since 2021-05-08
     * @return com.huawei.it.external.service.ITWoRemoteService ITWoRemoteService代理对象
     * @throws ApplicationException 创建代理后,类型转换异常时抛出异常
     */
    @Override
    public ITWoRemoteService getObject() throws ApplicationException {
        Object proxy = Proxy.newProxyInstance(ITWoRemoteService.class.getClassLoader(),
                        new Class[] {ITWoRemoteService.class}, new RemoteRequestInvocationHandler());
        if (proxy instanceof ITWoRemoteService) {
            return (ITWoRemoteService) proxy;
        }
        throw new BizApplicationException("======>>> ITWoRemoteServiceFactoryBean创建代理失败");
    }

    /**
     * 功能描述:获取当前Bean的类型,当前Bean的类型为ITwoRemoteService.class
     *
     * @author YaoJiang-wx1047757
     * @since 2021-05-08
     * @return java.lang.Class Bean的类型
     */
    @Override
    public Class<?> getObjectType() {
        return ITWoRemoteService.class;
    }

    /**
     * 功能描述:具体代理执行类,包含了对异常捕捉的动作
     *
     * @author YaoJiang-wx1047757
     * @since 2021-05-08
     */
    static class RemoteRequestInvocationHandler implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
                LOGGER.info2("======>>> ITWoRemoteService准备发起远程调用,method:{},参数:{}",
                        method.getName(), JSON.toJSONString(args));
                return method.invoke(REMOTE_TARGET, args);
            } catch (InvocationTargetException e) {
                LOGGER.error2("======>>> ITWoRemoteService调用发生异常,实际异常类型:{},异常堆栈信息:",
                        e.getTargetException().getClass().getName(), e.getTargetException());
                // 获取具体异常信息
                Throwable te = e.getTargetException();

                // 5xx异常处理
                if (te instanceof HttpServerErrorException) {
                    HttpServerErrorException he = (HttpServerErrorException) te;
                    String statusText = he.getStatusText();

                    // 异常信息截取 WO系统中直接使用throw new BizApplicationException("异常信息"),实际异常信息是封装在code中
                    // 异常信息格式为:{code=JALOR_EX$异常信息$(unknown), message=标准异常术语}
                    String errorMsg = null;
                    if (!StringUtils.isEmpty(statusText)) {
                        if (!statusText.contains(UNKNOWN) && statusText.contains(ERRCODE_PREFIX)) {
                            int errorCodeStart = statusText.indexOf(ERRCODE_PREFIX) + PREFIX_LENGTH;
                            int errorCodeEnd = statusText.indexOf("$", errorCodeStart);
                            errorMsg = statusText.substring(errorCodeStart, errorCodeEnd);
                        }
                    }
                    if (StringUtils.isEmpty(errorMsg)) {
                        errorMsg = "服务调用发生异常,请稍后重试";
                    }
                    throw new BizApplicationException(errorMsg);
                }

                // 4xx异常处理
                if (te instanceof HttpClientErrorException) {
                    throw new BizApplicationException("远程服务网络不通,或您无权限进行相关操作");
                }

                // 其他异常处理
                throw new BizApplicationException("发生未知异常,请联系相关人员");
            }
        }
    }
}

项目初始化 加载bean

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

package com.huawei.it.external.clock;

import com.huawei.it.external.clock.core.Node;
import com.huawei.it.external.clock.core.NodeContext;
import com.huawei.it.external.clock.core.NodeHandler;
import com.huawei.it.external.enums.RfcClockNodeEnum;
import com.huawei.it.jalor5.core.exception.ApplicationException;
import com.huawei.it.jalor5.core.log.ILogger;
import com.huawei.it.jalor5.core.log.JalorLoggerFactory;
import com.huawei.it.util.excetion.NoSuchNodeApplicationException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.StringUtils;

import javax.inject.Named;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
 * 服务于RFC业务的打卡节点上下文,打卡节点集由枚举导入
 * 提供能力:从指定枚举中构建节点前后映射关系、构建节点与处理器映射关系、节点查询
 *
 * @author YaoJiang-wx1047757
 * @since 2021-04-20
 */
@Named
public class RfcSimpleClockNodeContext implements NodeContext, ApplicationContextAware, InitializingBean {
    private static final ILogger logger = JalorLoggerFactory.getLogger(RfcSimpleClockNodeContext.class);

    // 节点处理器Bean的前缀
    private static final String BEAN_PREFIX = "_NODE_HANDLER_";

    // 所有打卡节点映射关系
    private static final Map<String, RfcClockNodeEnum> ENUM_MAP = RfcClockNodeEnum.mapEnum();

    // 所有实现了NodeHandler接口的Bean
    private Map<String, NodeHandler> handlerMap;

    // 节点code与节点映射关系
    private Map<String, Node> nodeMap;

    @Override
    public void setNodeSet() throws NoSuchNodeApplicationException {
        if (Objects.isNull(nodeMap)) {
            nodeMap = new HashMap();
        }
        wrapNode();
    }

    /**
     * 功能描述:封装节点上下文信息
     * 该方法是一个空壳方法,只是为了提供一个起始节点给wrapNode(RfcClockNodeEnum)进行递归
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @throws NoSuchNodeApplicationException 传入的初始节点不存在时抛出异常
     */
    private void wrapNode() throws NoSuchNodeApplicationException {
        RfcClockNodeEnum curNode = RfcClockNodeEnum.NONE;
        wrapNode(curNode);
    }

    /**
     * 功能描述:封装节点上下文信息
     * 该方法进行递归调用,从RfcClockNodeEnum枚举中解析节点的上下文关联关系
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param curNode 当前节点
     * @return com.huawei.it.external.clock.core.Node 节点信息
     * @throws NoSuchNodeApplicationException 传入的初始节点不存在时抛出异常
     */
    private Node wrapNode(RfcClockNodeEnum curNode) throws NoSuchNodeApplicationException {
        Node node = new Node();
        node.setNodeId(curNode.getCode());

        // 组装节点处理器
        NodeHandler handler = handlerMap.get(BEAN_PREFIX + curNode.getCode());
        node.setHandler(handler);

        // 组装节点前后映射关系
        String nextItemStr = curNode.getNext();
        if (!StringUtils.isEmpty(nextItemStr)) {
            String[] nextItems = nextItemStr.split(",");
            for (String item : nextItems) {
                // 可能存在节点已经实例化的场景,这里需要排除
                Node itemNode = nodeMap.get(item.trim());
                if (Objects.isNull(itemNode)) {
                    RfcClockNodeEnum nextNode = ENUM_MAP.get(item.trim());
                    if (Objects.isNull(nextNode)) {
                        throw new NoSuchNodeApplicationException("未找到指定打卡节点");
                    }

                    // 这里涉及到递归
                    itemNode = wrapNode(nextNode);
                }

                // 节点前后关系
                itemNode.addPre(node);
                node.addPost(itemNode);
            }
        }
        logger.info2("The context of node {} has been loaded.", node.getNodeId());
        nodeMap.put(curNode.getCode(), node);
        return node;
    }

    @Override
    public void refresh() {
        if (Objects.isNull(nodeMap)) {
            nodeMap = new HashMap();
        } else {
            nodeMap.clear();
        }
        logger.info2("The node context has been clear. Next we will refresh the node context.");
        logger.info2("The node context refresh strategy has not been defined");
    }

    @Override
    public Node currentNode(String nodeId) throws NoSuchNodeApplicationException {
        return Optional.ofNullable(nodeMap.get(nodeId))
                .orElseThrow(() -> new NoSuchNodeApplicationException("未找到指定的打卡节点"));
    }

    @Override
    public List<Node> preNodes(String nodeId) throws NoSuchNodeApplicationException {
        return Optional.ofNullable(currentNode(nodeId).getPre()).orElse(new ArrayList(0));
    }

    @Override
    public List<Node> postNodes(String nodeId) throws NoSuchNodeApplicationException {
        return Optional.ofNullable(currentNode(nodeId).getPost()).orElse(new ArrayList(0));
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        if (Objects.isNull(handlerMap)) {
            handlerMap = new HashMap();
        }
        Map<String, NodeHandler> handlerBeans = applicationContext.getBeansOfType(NodeHandler.class);
        for (NodeHandler bean : handlerBeans.values()) {
            if (StringUtils.isEmpty(bean.getName())) {
                logger.error(
                        "Please provide implementation for NodeHandler.getName(),"
                                + "the return value will be the bean name of the container.");
            }
            handlerMap.put(BEAN_PREFIX + bean.getName(), bean);
        }
    }

    @Override
    public void afterPropertiesSet() throws ApplicationException {
        // 触发组装操作
        setNodeSet();
    }
}

抽象类父类方法

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

package com.huawei.it.external.clock;

import com.alibaba.fastjson.JSON;
import com.huawei.it.external.clock.core.AbstractNodeHandler;
import com.huawei.it.external.clock.core.Node;
import com.huawei.it.external.clock.core.ParamSource;
import com.huawei.it.external.entity.RFCPlanEntity;
import com.huawei.it.external.enums.ParamSourceEnum;
import com.huawei.it.external.enums.RfcClockNodeEnum;
import com.huawei.it.external.enums.RfcOpStatusEnum;
import com.huawei.it.external.model.rfc.RFCClockInfoVO;
import com.huawei.it.external.model.wovo.TWoAddress;
import com.huawei.it.external.model.wovo.TWoClock;
import com.huawei.it.external.model.wovo.TWoFile;
import com.huawei.it.external.service.ITWoRemoteService;
import com.huawei.it.external.utils.ApplicationContextUtils;
import com.huawei.it.external.utils.DistancesUtils;
import com.huawei.it.jalor5.core.exception.ApplicationException;
import com.huawei.it.jalor5.core.log.ILogger;
import com.huawei.it.jalor5.core.log.JalorLoggerFactory;
import com.huawei.it.jalor5.core.request.IUserPrincipal;
import com.huawei.it.jalor5.core.request.impl.RequestContext;
import com.huawei.it.util.excetion.BizApplicationException;
import com.huawei.it.util.util.DateUtils;
import org.springframework.util.StringUtils;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * 打卡节点抽象接口
 * 能力:提供执行打卡逻辑执行前、打卡逻辑执行后的扩展能力
 *
 * @author YaoJiang-wx1047757
 * @since 2021-04-22
 */
public abstract class AbstractClockNodeHandler extends AbstractNodeHandler {
    private static final ILogger logger = JalorLoggerFactory.getLogger(AbstractClockNodeHandler.class);

    // 变更计划状态限制,状态为Implement才允许执行打卡操作
    private static final String STATUS_LIMIT = "Implement";

    // 远程调用服务类Bean名称
    private static final String REMOTE_SERVICE = "tWoRemoteService";

    /**
     * 功能描述:该方法提供打卡操作前的验证工作,包含但不限于
     * (1)变更单信息及状态检测(状态需要为Implement)
     * (2)打卡用户检测(打卡工程师与当前登录工程师需为同一人)
     * (3)打卡节点检测(检测上一个打卡节点,检测当前变更单状态)
     * (4)打卡重复检测(任何打卡节点不允许重复打卡)
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param source 由接口调用者传入的参数源
     * @throws ApplicationException 可能抛出ApplicationException异常
     */
    @Override
    public void before(ParamSource source) throws ApplicationException {
        // 变更单信息及状态检测
        checkRfc(source);

        // 打卡用户检测
        checkAuth(source);

        // 打卡节点检测
        checkClockNode(source);

        // 打卡重复检测
        checkRepeat(source);
    }

    /**
     * 功能描述:变更计划是否存在及状态检测
     * 业务要求:
     * (1)变更单状态应为非Implement只能执行出发打卡
     * (2)开始打卡时间早于计划开始时间需要填写备注
     * (3)完成打卡时间晚于计划完成时间需要填写备注
     * 检测通过后,将需要回传到ECare的变更计划信息封装在source中
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param source 由接口调用者传入的参数源
     * @throws ApplicationException 可能抛出ApplicationException异常
     */
    private void checkRfc(ParamSource source) throws ApplicationException {
        // 获取变更计划详情
        RFCPlanEntity planEntity =
                Optional.ofNullable(source.get(ParamSourceEnum.PLAN.getName(), RFCPlanEntity.class))
                        .orElseThrow(() -> new BizApplicationException("未找到指定变更计划信息"));
        if (Objects.isNull(planEntity.getRfcPlanNo())) {
            throw new BizApplicationException("未找到指定变更计划信息");
        }

        // 获取当前打卡节点
        TWoClock tWoClock = source.get(ParamSourceEnum.CLOCK.getName(), ParamSourceEnum.CLOCK.getType());

        logger.info2("======>>> 当前变更计划单单号[{}],计划单状态[{}],计划单操作状态[{}]",
                planEntity.getRfcPlanNo(), planEntity.getStatus(), planEntity.getOperationStatus());
        logger.info2("======>>> 变更计划单[{}]打卡,入参:{}", planEntity.getRfcPlanNo(), JSON.toJSONString(tWoClock));

        // 核心动作:判断当前变更计划状态是否为Implement
        // 判断规则:变更计划状态为非Implement时可以打出发卡/到达卡但不能打开始卡,为Implement时可以进行任意打卡操作
        if (!STATUS_LIMIT.equals(planEntity.getStatus()) &&
                !RfcClockNodeEnum.SETOUT.getCode().equals(tWoClock.getPunchType()) &&
                !RfcClockNodeEnum.ARRIVED.getCode().equals(tWoClock.getPunchType())) {
            throw new BizApplicationException("该变更单未到实施状态,无法打卡,请先完成变更单三授权等必要操作");
        }

        // 核心动作:判断开始卡时间是否在计划开始时间前,如果在计划开始时间前,则需要填写备注
        String plannedSart = planEntity.getPlannedSart();
        if (!StringUtils.isEmpty(plannedSart) && RfcClockNodeEnum.START.getCode().equals(tWoClock.getPunchType())) {
            Date planStartDate = DateUtils.paresStringToData(plannedSart, "yyyy-MM-dd HH:mm:ss");
            if (planStartDate.compareTo(tWoClock.getPunchTime()) > 0 && StringUtils.isEmpty(tWoClock.getFailNote())) {
                throw new BizApplicationException("变更实际开始时间早于计划开始时间,请备注原因");
            }
        }

        // 核心动作:判断完成卡时间是否在计划完成时间后,如果在计划完成时间后,则需要填写备注
        String plannedEnd = planEntity.getPlannedEnd();
        if (!StringUtils.isEmpty(plannedEnd) &&
                (RfcClockNodeEnum.COMPLETE_SUC.getCode().equals(tWoClock.getPunchType()) ||
                        RfcClockNodeEnum.COMPLETE_FAIL.getCode().equals(tWoClock.getPunchType()))) {
            Date planEndDate = DateUtils.paresStringToData(plannedEnd, "yyyy-MM-dd HH:mm:ss");
            if (planEndDate.compareTo(tWoClock.getPunchTime()) < 0 && StringUtils.isEmpty(tWoClock.getFailNote())) {
                throw new BizApplicationException("变更实际完成时间晚于计划完成时间,请备注原因");
            }
        }

        // 封装回传到ECare的RFCPlanEntity信息
        RFCPlanEntity saveEntity = new RFCPlanEntity();
        saveEntity.setRfcNo(planEntity.getRfcNo());
        saveEntity.setRfcPlanNo(planEntity.getRfcPlanNo());
        source.set(ParamSourceEnum.SAVE_PLAN.getName(), saveEntity);
    }

    /**
     * 功能描述:当前打卡用户检测
     * 业务要求当前打卡人与变更计划中的实施工程师需为同一人
     * 检测通过后,将当前打卡人id封装到打卡信息实体中
     * 备注:2021-04-23 该检测节点暂不开启,仅将当前登录人信息封装到source中
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param source 由接口调用者传入的参数源
     * @throws ApplicationException 可能抛出ApplicationException异常
     */
    private void checkAuth(ParamSource source) {
        // 获取当前登录人(即当前打卡人),将当前登录人信息封装在source中
        IUserPrincipal principal = RequestContext.getCurrent().getUser();
        source.set(ParamSourceEnum.USER.getName(), principal);
    }

    /**
     * 功能描述:打卡节点检测,判断当前打卡人能否执行该节点打卡操作
     * 业务要求打卡节点之间有前后关系,具体参照RfcClockNodeEnum中的定义
     * 节点检测需要关注两部分:
     * (1)至少执行上一级打卡节点中的一个后,才能执行当前打卡操作
     * (2)变更计划当前操作状态需要满足当前打卡节点需求,具体参考RfcOperationStatusEnum中的定义
     * 这两个关注点需要同时满足才能执行打卡操作
     * 如果当前判断方式不符合具体业务需求,可以在子类中重写该方法
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param source 由接口调用者传入的参数源
     * @throws ApplicationException 可能抛出ApplicationException异常
     */
    protected void checkClockNode(ParamSource source) throws ApplicationException {
        // 从source中取出当前节点上下文/当前打卡信息
        Node currentNode = source.get(ParamSourceEnum.NODE.getName(), ParamSourceEnum.NODE.getType());
        TWoClock tWoClock = source.get(ParamSourceEnum.CLOCK.getName(), ParamSourceEnum.CLOCK.getType());

        // 获取已打卡节点列表
        ITWoRemoteService remoteService = ApplicationContextUtils.getBean(REMOTE_SERVICE, ITWoRemoteService.class);
        List<TWoClock> tWoClockList = remoteService.findClockList(tWoClock.getTaskId());

        // 核心动作:获取当前打卡节点所需的前置节点列表,判断已打卡节点中是否在其中存在
        // 注意:只需要有一个已打卡节点在前置节点列表中即可,出发打卡无需检测
        if (!RfcClockNodeEnum.SETOUT.getCode().equals(currentNode.getNodeId())) {
            List<String> preNodes = currentNode.getPre().stream().map(Node::getNodeId).collect(Collectors.toList());
            tWoClockList.stream().map(TWoClock::getPunchType).distinct().filter(node -> preNodes.contains(node))
                    .limit(1).findFirst().orElseThrow(() ->
                    new BizApplicationException("请在完成[" + String.join(",", preNodes) + "]打卡后重新操作"));
        }

        // 从source中获取变更计划信息
        RFCPlanEntity planEntity = source.get(ParamSourceEnum.PLAN.getName(), ParamSourceEnum.PLAN.getType());
        /**
         * 打卡节点校验通过后,判断变更计划当前操作状态是否符合要求(尤其是离场卡,需要操作状态为Cancelled)
         * 1. 通过当前节点找到对应的打卡节点枚举,枚举中映射了打卡节点与操作状态的状态值
         * 2. 根据打卡节点枚举获取到当前打卡节点所需的操作状态
         * 满足一个打卡节点的的变更状态可能存在多个,只需要检测存在其中一个即可
         */
        RfcClockNodeEnum clockNodeEnum = RfcClockNodeEnum.byCode(currentNode.getNodeId());
        List<RfcOpStatusEnum> needStatus = clockNodeEnum.getNeedStatus();
        // 核心动作:只要当前变更计划的操作状态满足当前打卡节点所需状态的其中一个即可执行打卡
        needStatus.stream().filter(status -> status.getCode().equals(planEntity.getOperationStatus()))
                .limit(1).findFirst().orElseThrow(() ->
                new BizApplicationException("当前变更计划操作状态[" + planEntity.getOperationStatus() + "]不符合业务要求"));

        // 将已打卡数据放入source中,供后续判断使用
        source.set(ParamSourceEnum.CLOCKED.getName(), tWoClockList);
    }

    /**
     * 功能描述:打卡重复检测
     * 业务要求同一个打卡节点不允许重复打卡
     * 检测通过后,开始执行handle()方法
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param source 由接口调用者传入的参数源
     * @throws ApplicationException 可能抛出ApplicationException异常
     */
    private void checkRepeat(ParamSource source) throws ApplicationException {
        // 从source中取出当前登录人该变更计划所有的打卡记录/当前打卡节点
        List<TWoClock> tWoClockList = source.get(ParamSourceEnum.CLOCKED.getName(), List.class);
        Node currentNode = source.get(ParamSourceEnum.NODE.getName(), ParamSourceEnum.NODE.getType());

        // 核心动作:判断,已打卡记录中是否包含当前打卡节点,如果包含则抛出异常
        boolean clocked = tWoClockList.stream()
                .filter(clock -> currentNode.getNodeId().equals(clock.getPunchType()))
                .limit(1).findFirst().isPresent();
        if (clocked) {
            throw new BizApplicationException("您在该打卡节点已完成打卡,请勿重复操作");
        }
    }

    /**
     * 功能描述:判断打卡位置,该方法不允许被重写
     * 业务要求工程师打卡位置与客户位置之间距离不超过3公里
     * 注意事项:该方法执行时序请安排在 before - method - handle
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-27
     * @param source 由接口调用者传入的参数源
     * @throws ApplicationException 当未提前在系统中补充客户地址信息或未填写异常信息时抛出异常
     */
    protected final void checkDistance(ParamSource source) throws ApplicationException {
        // 从参数源中获取当前打卡信息
        TWoClock tWoClock = source.get(ParamSourceEnum.CLOCK.getName(), ParamSourceEnum.CLOCK.getType());

        // 获取客户服务地址
        ITWoRemoteService remoteService = ApplicationContextUtils.getBean(REMOTE_SERVICE, ITWoRemoteService.class);
        TWoAddress address = remoteService.findAddressByTaskId(tWoClock.getTaskId());
        if (Objects.isNull(address)) {
            throw new BizApplicationException("请为变更计划单补充客户地址后再执行打卡操作");
        }

        // 核心动作:通过经纬度对比,判断打卡位置与客户位置之间是否超过3公里
        Double lat1 = Double.parseDouble(address.getLat() == null ? "0" : address.getLat());
        Double lng1 = Double.parseDouble(address.getLng() == null ? "0" : address.getLng());
        String orderAddress = address.getAddress() == null ? "" : address.getAddress();
        Double lat = Double.parseDouble(tWoClock.getLat());
        Double lng = Double.parseDouble(tWoClock.getLng());
        String punchAddress = tWoClock.getPunchAddress();
        Map<String, Object> addrMap = new HashMap<>();
        addrMap.put("punchAddress", punchAddress);
        addrMap.put("orderAddress", orderAddress);
        boolean compareResult = DistancesUtils.distancesMethod(lat, lng, lat1, lng1, addrMap);

        // 如果打卡位置与客户位置之间超过3公里,且没有填写打卡异常信息,则抛出异常
        if (!compareResult) {
            if (StringUtils.isEmpty(tWoClock.getFailNote()) || tWoClock.getFailNote().length() < 4) {
                throw new BizApplicationException("打卡地点异常,请填写打卡备注信息");
            }
        }
    }

    /**
     * 功能描述:封装打卡附件相关信息,该方法不允许被重写
     * 注意事项:该方法执行时序请安排在 before - handle - method
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-27
     * @param source 由接口调用者传入的参数源
     * @throws ApplicationException 如果检测到附件未上传或当前登录人与上传人不是同一人则抛出异常
     */
    protected final void formatTWoFile(ParamSource source) throws ApplicationException {
        // 从参数源中获取当前登录人/本次打卡信息
        IUserPrincipal currentUser = source.get(ParamSourceEnum.USER.getName(), ParamSourceEnum.USER.getType());
        TWoClock tWoClock = source.get(ParamSourceEnum.CLOCK.getName(), ParamSourceEnum.CLOCK.getType());

        // 获取本次打卡上传的附件信息
        if (StringUtils.isEmpty(tWoClock.getEdmId())) {
            throw new BizApplicationException("请上传打卡附件");
        }
        ITWoRemoteService remoteService = ApplicationContextUtils.getBean(REMOTE_SERVICE, ITWoRemoteService.class);
        TWoFile tWoFile = remoteService.findTWoFileByEdmId(tWoClock.getEdmId());
        if (Objects.isNull(tWoFile)) {
            throw new BizApplicationException("未找到打卡附件相关信息");
        }

        // 如果当前登录人与附件上传人不是同一人则抛出异常
        if ((double) currentUser.getUserId() != tWoFile.getCreatedBy()) {
            throw new BizApplicationException("当前操作人与附件上传人不匹配");
        }

        // 核心动作:将当前打卡节点id作为关联id回写到附件信息
        tWoFile.setCorrelativeId(tWoClock.getTaskId());
        tWoFile.setLastUpdateDate(new Date());

        // 将附件文件信息保存到source中
        source.set(ParamSourceEnum.FILE.getName(), tWoFile);
    }

    @Override
    public void handle(ParamSource source) throws ApplicationException {
        // 从参数源中获取本次打卡信息/当前登录用户信息
        TWoClock tWoClock = source.get(ParamSourceEnum.CLOCK.getName(), ParamSourceEnum.CLOCK.getType());
        IUserPrincipal principal = source.get(ParamSourceEnum.USER.getName(), ParamSourceEnum.USER.getType());

        // 封装当前当卡信息相关参数
        Date nowDate = new Date();
        String clockId = UUID.randomUUID().toString();
        tWoClock.setId(clockId);
        tWoClock.setCreateDate(nowDate);
        tWoClock.setLastUpdateDate(nowDate);
        tWoClock.setCreatedBy((double) principal.getUserId());
        tWoClock.setLastUpdateBy((double) principal.getUserId());

        // 保存打卡信息
        source.set(ParamSourceEnum.CLOCK.getName(), tWoClock);
    }

    /**
     * 功能描述:该方法提供打卡操作后的工作,包含但不限于
     * (1)封装向ECare回传的打卡数据
     * (2)调用远程服务将打卡信息、打卡日志持久化并回调ECare
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param source 由接口调用者传入的参数源
     * @throws ApplicationException 可能抛出ApplicationException异常
     */
    @Override
    public void after(ParamSource source) throws ApplicationException {
        // 封装回传到ECare的打卡信息
        formatECareClockInfo(source);

        // 执行远程请求
        doRemoteRequest(source);

        // 执行完毕返回结果
        source.set(ParamSourceEnum.RETURN.getName(), "打卡成功");
    }

    /**
     * 功能描述:封装回传到ECare的打卡信息
     * 该接口每个打卡类型需要回传的字段不同,由各个子类自行实现
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param source 由接口调用者传入的参数源
     * @throws ApplicationException 可能抛出ApplicationException异常
     */
    protected abstract void formatECareClockInfo(ParamSource source) throws ApplicationException;

    /**
     * 功能描述:执行远程请求
     * 注意事项:这里默认实现将打卡信息、日志信息落地的操作和回调ECare的操作统一放到WO去执行
     * 原因:
     * (1)如果将打卡信息、日志信息的落地操作放WO侧执行,回调ECare的操作放到ASP侧,这时候如果
     * WO侧执行成功,但ASP侧执行失败,则可能产生数据状态不一致的情况
     * (2)如果将打卡信息、日志信息落地的操作和回调ECare的操作统一放到WO去执行,那么在WO侧依次
     * 执行打卡信息持久化、日志信息持久化、回调ECare操作,这时前两步操作是本地数据库操作,可以放
     * 在同一个事务内,三个步骤任何一个发生异常都会触发事务的回滚,以此来保证数据状态一致性
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-26
     * @param source 由接口调用者传入的参数源
     */
    private void doRemoteRequest(ParamSource source) {
        // 从source中获取远程调用所需的参数
        TWoClock tWoClock = source.get(ParamSourceEnum.CLOCK.getName(), ParamSourceEnum.CLOCK.getType());
        RFCPlanEntity saveEntity = source.get(ParamSourceEnum.SAVE_PLAN.getName(), ParamSourceEnum.SAVE_PLAN.getType());
        TWoFile tWoFile = source.get(ParamSourceEnum.FILE.getName(), ParamSourceEnum.FILE.getType());

        // 组装远程调用参数
        RFCClockInfoVO clockInfoVO = new RFCClockInfoVO();
        clockInfoVO.setTWoClock(tWoClock);
        clockInfoVO.setPlanEntity(saveEntity);
        clockInfoVO.setTWoFile(tWoFile);

        // 发起远程调用
        ITWoRemoteService remoteService = ApplicationContextUtils.getBean(REMOTE_SERVICE, ITWoRemoteService.class);

        // 计算ASP侧耗时信息
        Long startTimestamp = source.get(ParamSourceEnum.EXE_START.getName(), ParamSourceEnum.EXE_START.getType());
        Long nowTimestamp = System.currentTimeMillis();
        logger.info2("======>>> 变更计划单号:[{}],本次打卡asp-external侧耗时:{}ms",
                saveEntity.getRfcPlanNo(), nowTimestamp - startTimestamp);
        logger.info2("======>>> 远程调用ITWoRemoteService,入参:", JSON.toJSONString(clockInfoVO));
        remoteService.addRfcClockInfo(clockInfoVO);
        logger.info2("======>>> 变更计划单号:[{}],asp-external侧远程调用耗时:{}ms",
                saveEntity.getRfcPlanNo(), System.currentTimeMillis() - nowTimestamp);
    }
}

子类一

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

package com.huawei.it.external.clock.handler;

import com.huawei.it.external.clock.AbstractClockNodeHandler;
import com.huawei.it.external.clock.core.ParamSource;
import com.huawei.it.external.entity.RFCPlanEntity;
import com.huawei.it.external.enums.ParamSourceEnum;
import com.huawei.it.external.enums.RfcClockNodeEnum;
import com.huawei.it.external.model.wovo.TWoClock;
import com.huawei.it.jalor5.core.exception.ApplicationException;
import com.huawei.it.util.util.DateUtil;

import javax.inject.Named;

/**
 * 功能描述:处理器-打卡节点[到场]
 *
 * @author YaoJiang-wx1047757
 * @since 2021-04-22
 */
@Named
public class ClockNodeArrivedHandler extends AbstractClockNodeHandler {
    @Override
    public void handle(ParamSource source) throws ApplicationException {
        // 在调用父类的handle()方法前先检验打卡地址
        super.checkDistance(source);
        super.handle(source);

        // 打到达卡时会上传附件,因此需要处理附件
        super.formatTWoFile(source);
    }

    @Override
    protected void formatECareClockInfo(ParamSource source) throws ApplicationException {
        // 从参数源中获取打卡信息/变更操作信息
        TWoClock tWoClock = source.get(ParamSourceEnum.CLOCK.getName(), ParamSourceEnum.CLOCK.getType());
        RFCPlanEntity planEntity = source.get(ParamSourceEnum.SAVE_PLAN.getName(), ParamSourceEnum.SAVE_PLAN.getType());

        // 封装与ECare交互的打卡信息
        String punchTime = DateUtil.getDateString(tWoClock.getPunchTime());
        planEntity.setActualArrive(punchTime);
        planEntity.setActualArriveAddress(tWoClock.getPunchAddress());

        // 将封装后的回传信息保存到参数源中
        source.set(ParamSourceEnum.PLAN.getName(), planEntity);
    }

    @Override
    public String getName() {
        return RfcClockNodeEnum.ARRIVED.getCode();
    }
}

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

package com.huawei.it.external.clock.core;

import com.huawei.it.jalor5.core.exception.ApplicationException;

/**
 * 抽象节点处理器,用户至少需要提供handle方法的实现
 * 能力:封装处理流程 前置处理 --> 业务处理 --> 后置处理
 *
 * @author YaoJiang-wx1047757
 * @since 2021-04-20
 */
public abstract class AbstractNodeHandler implements NodeHandler {
    /**
     * 功能描述:NodeHandler接口的默认实现,规定了业务处理流程为:前置处理 --> 业务处理 --> 后置处理
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param source 由接口调用者传入的参数源
     * @throws ApplicationException 可能抛出ApplicationException异常
     */
    public void doHandler(ParamSource source) throws ApplicationException {
        // 前置处理
        before(source);

        // 实际业务处理
        handle(source);

        // 后置处理
        after(source);
    }

    /**
     * 功能描述:对节点处理前时机点进行增强
     * 需要在节点处理前做一些初始化操作重写该方法
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param source 由接口调用者传入的参数源
     * @throws ApplicationException 可能抛出ApplicationException异常
     */
    public void before(ParamSource source) throws ApplicationException {}

    /**
     * 功能描述:节点处理方法,执行时机早于after
     * 具体的节点处理逻辑在该方法中实现
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param source 由接口调用者传入的参数源
     * @throws ApplicationException 可能抛出ApplicationException异常
     */
    public abstract void handle(ParamSource source) throws ApplicationException;

    /**
     * 功能描述:后置处理方法,晚于handle()方法执行
     * 需要在节点处理后做一些收尾操作重写该方法
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param source 由接口调用者传入的参数源
     * @throws ApplicationException 可能抛出ApplicationException异常
     */
    public void after(ParamSource source) throws ApplicationException {}
}

node

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

package com.huawei.it.external.clock.core;

import com.huawei.it.external.enums.ParamSourceEnum;
import com.huawei.it.jalor5.core.exception.ApplicationException;

import lombok.Data;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * 节点,包含节点上下级映射关系
 *
 * @author YaoJiang-wx1047757
 * @since 2021-04-20
 */
@Data
public class Node {
    // 当前节点唯一标识,参照枚举ClockNodeEnum
    private String nodeId;

    // 前置节点,表示要达到当前节点可能经过的节点
    private List<Node> pre;

    // 后置节点,表示当前节点未来可选择的节点
    private List<Node> post;

    // 当前节点的内置处理器
    private NodeHandler handler;

    /**
     * 功能描述:将当前节点信息封装在参数源中,并执行节点内置处理器
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param source 由接口调用者传入的参数源
     * @throws ApplicationException 可能抛出ApplicationException异常
     */
    public void executeHandler(ParamSource source) throws ApplicationException {
        if (Objects.nonNull(handler)) {
            source.set(ParamSourceEnum.NODE.getName(), this);
            handler.doHandler(source);
        }
    }

    /**
     * 功能描述:为当前节点添加一个前置节点
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param node 需要添加的前置节点
     */
    public void addPre(Node node) {
        if (Objects.isNull(pre)) {
            this.pre = new ArrayList();
        }
        this.pre.add(node);
    }

    /**
     * 功能描述:为当前节点添加一个后置节点
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param node 需要添加的后置节点
     */
    public void addPost(Node node) {
        if (Objects.isNull(post)) {
            this.post = new ArrayList();
        }
        this.post.add(node);
    }
}

NodeContext

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

package com.huawei.it.external.clock.core;

import com.huawei.it.util.excetion.NoSuchNodeApplicationException;

import java.util.List;

/**
 * 节点上下文
 * 该接口是节点上下文的顶层接口,提供上下文构建、上下文刷新、上下文获取的能力
 * 重要提示:
 * 1. 类关联关系及重要方法
 * NodeContext - Node - NodeHandler三者是绑定关系
 * 通过调用setNodeSet()方法从指定数据源加载节点上下文,节点信息及对应的处理器信息被封装到Node对象中
 * 通过调用refresh()方法清空当前节点上下文中的所有节点信息,并从指定数据源重新加载
 * 2. 系统启动后工作流程
 * NodeContext中维护了Node与NodeHandler之间的映射关系,其工作流程为:
 * (1)NodeContext.currentNode(nodeId)获取到指定的节点
 * (2)node.executeHandler(source)执行节点的具体处理逻辑
 * (3)NodeHandler中的doHandler(source)方法实现中编写具体实现,其抽象AbstractNodeHandler将来处理流程分为before/handle/after
 * (4)使用者可以继承AbstractNodeHandler,重写after/handle/after方法,对公用逻辑等进行自由提取封装
 * 3. 扩展自己的节点上下文
 * (1)实现NodeContext接口,建议同时实现ApplicationContextAware/InitializingBean接口,实现处理器解析/系统启动自动加载的能力
 * (2)为接口方法setNodeSet()/refresh()方法提供具体实现,setNodeSet()方法提供上下文构建,refresh()方法提供上下文刷新能力
 * (3)将该实现类添加@Named注解,方便其能被使用的Spring容器框架扫描到并被注册为容器中的可用Bean
 * 4. 新增处理节点流程(该流程符合开闭原则)
 * (1)在数据源中配置一个新的节点
 * (2)创建节点处理器,该处理器实现NodeHandler接口,必须实现getName()方法,并在类上添加@Named注解
 *     getName()方法的返回值将作为该类的实例在NodeContext中的映射键,用于与数据源中的节点进行关联
 * 系统启动时会从主容器中获取到NodeHandler类型的Bean,然后将其放到NodeContext中构建上下文关系
 *
 * @author YaoJiang-wx1047757
 * @since 2021-04-20
 */
public interface NodeContext {
    /**
     * 功能描述:功能描述:设置当前上下文中的节点集合,数据来源自定,可以通过枚举,也可以自行通过数据库导入
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @throws NoSuchNodeApplicationException 从数据源中未找到指定节点时抛出异常
     */
    void setNodeSet() throws NoSuchNodeApplicationException;

    /**
     * 功能描述:刷新当前节点集合
     * 包含动作:清空节点集、清空处理器集、重新加载节点集、重新加载处理器集
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     */
    void refresh();

    /**
     * 功能描述:根据nodeId获取当前节点信息,包含该节点的前置后后置节点
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param nodeId 节点的唯一键
     * @return com.huawei.it.external.clock.core.Node 查询到的节点信息
     * @throws NoSuchNodeApplicationException 未找到指定nodeId的节点时抛出异常
     */
    Node currentNode(String nodeId) throws NoSuchNodeApplicationException;

    /**
     * 功能描述:根据nodeId获取当前节点的前置节点列表
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param nodeId 节点的唯一键
     * @return java.util.List<com.huawei.it.external.clock.core.Node> 查询到的节点的上级节点列表
     * @throws NoSuchNodeApplicationException 未找到指定nodeId的节点时抛出异常
     */
    List<Node> preNodes(String nodeId) throws NoSuchNodeApplicationException;

    /**
     * 功能描述:根据nodeId获取当前节点的后置节点列表
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param nodeId 节点的唯一键
     * @return java.util.List<com.huawei.it.external.clock.core.Node> 查询到的节点的下级节点列表
     * @throws NoSuchNodeApplicationException 未找到指定nodeId的节点时抛出异常
     */
    List<Node> postNodes(String nodeId) throws NoSuchNodeApplicationException;
}

节点处理器

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

package com.huawei.it.external.clock.core;

import com.huawei.it.jalor5.core.exception.ApplicationException;

/**
 * 节点处理器,由指定节点持有
 * 抽象实现有AbstractNodeHandler,定义了before/handle/after方法,用于事前、事中、事后处理
 * 能力:节点环绕逻辑执行
 *
 * @author YaoJiang-wx1047757
 * @since 2021-04-20
 */
public interface NodeHandler {
    /**
     * 功能描述:获取当前节点处理器名称
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @return 节点处理器名称
     */
    String getName();

    /**
     * 功能描述:执行节点处理
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param source 由使用者设置的参数源
     * @throws ApplicationException 可能抛出ApplicationException异常
     */
    void doHandler(ParamSource source) throws ApplicationException;
}

属性拾取器

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

package com.huawei.it.external.clock.core;

/**
 * 功能描述:参数源,提供参数暂存/参数查找/参数类型转换的能力
 *
 * @author YaoJiang-wx1047757
 * @since 2021-04-23
 */
public interface ParamSource {
    /**
     * 功能描述:根据name查找指定的对象或参数值,然后将其转换为指定类型后返回
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-23
     * @param name 对象或参数值对应的名称
     * @param type 对象或参数值需要转换成的类型
     * @return T 返回指定类型的对象
     */
    <T> T get(String name, Class<T> type);

    /**
     * 功能描述:向参数源中设置指定的对象或参数值
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-23
     * @param name 对象或参数值名称
     * @param object 具体的对象或参数值
     */
    <T> void set(String name, T object);
}

单例属性拾取

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

package com.huawei.it.external.clock.core;

import java.util.HashMap;
import java.util.Map;

/**
 * 功能描述:ParamSource的HashMap实现,底层使用的数据结构为HashMap
 * 提供能力:元素新增、元素获取(获取后不销毁)
 * 注意事项:传入参数时,参数name请注意不要重复,Hash冲突会导致性能下降
 *
 * @author YaoJiang-wx1047757
 * @since 2021-04-23
 */
public class SimpleMapParamSource implements ParamSource {
    // Map结构用于保存传入的数据
    private Map<String, Object> paramMap = new HashMap();

    @Override
    public <T> T get(String name, Class<T> type) {
        return (T) paramMap.get(name);
    }

    @Override
    public <T> void set(String name, T object) {
        paramMap.put(name, object);
    }
}

接口请求处理

 @AvoidResubmit
    @Override
    public ResponseEntity addClock(TWoClock tWoClock) throws ApplicationException {
        if (StringUtils.isEmpty(tWoClock.getTaskId())) {
            throw new BizApplicationException("参数缺失,请绑定需要执行打卡操作的变更计划单");
        }
        Date nowDate = new Date();

        // 填充打卡时间
        tWoClock.setPunchTime(nowDate);

        // 获取当前变更操作单信息
        RFCPlanEntity planEntityQuery = new RFCPlanEntity();
        planEntityQuery.setRfcPlanNo(tWoClock.getTaskId());
        RFCPlanEntity planEntity = rfcListService.queryRfcPlanList(planEntityQuery);

        // 封装参数源
        ParamSource source = new SimpleMapParamSource();
        source.set(ParamSourceEnum.CLOCK.getName(), tWoClock);
        source.set(ParamSourceEnum.PLAN.getName(), planEntity);
        source.set(ParamSourceEnum.EXE_START.getName(), nowDate.getTime());

        // 调用打卡组件执行打卡操作
        nodeContext.currentNode(tWoClock.getPunchType()).executeHandler(source);

        // 从参数源中获取打卡结果返回
        return ResponseEntity.ok(source.get(ParamSourceEnum.RETURN.getName(), ParamSourceEnum.RETURN.getType()));
    }

属性枚举

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

package com.huawei.it.external.enums;

import com.huawei.it.external.clock.core.Node;
import com.huawei.it.external.entity.RFCPlanEntity;
import com.huawei.it.external.model.wovo.TWoClock;
import com.huawei.it.external.model.wovo.TWoFile;
import com.huawei.it.jalor5.core.request.IUserPrincipal;

import java.util.List;

/**
 * 功能描述:参数源枚举,对参数名及其对应Class之间的映射进行管理
 * 目的:防止开发过程中通过随机参数名使用ParamSource对象,造成打卡流程中传参不可控
 * 注意事项:该枚举仅适用于变更作业打卡,其他业务请自行订制
 *
 * @author YaoJiang-wx1047757
 * @since 2021-04-23
 */
public enum ParamSourceEnum {
    CLOCK("TWoClock", TWoClock.class, "当前打卡信息"),
    FILE("TWoFile", TWoFile.class, "打卡附件信息"),
    CLOCKED("Clocked", List.class, "已打卡记录列表"),
    USER("Principal", IUserPrincipal.class, "当前登录用户"),
    PLAN("PlanEntity", RFCPlanEntity.class, "变更计划单信息"),
    SAVE_PLAN("SaveEntity", RFCPlanEntity.class, "回传到ECare的实体"),
    NODE("CurrentNode", Node.class, "当前打卡节点"),
    RETURN("ReturnValue", Object.class, "返回值"),
    EXE_START("StartExecute", Long.class, "开始执行时间");

    // 参数名称
    private String name;

    // 参数类型
    private Class type;

    // 参数描述
    private String desc;

    ParamSourceEnum(String name, Class type, String desc) {
        this.name = name;
        this.type = type;
        this.desc = desc;
    }

    /**
     * 功能描述:获取当前枚举映射的参数名称
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-23
     * @return java.lang.String 参数名称
     */
    public String getName() {
        return name;
    }

    /**
     * 功能描述:获取当前枚举映射的参数Class类型
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-23
     * @return java.lang.Class 参数Class类型
     */
    public <T> Class<T> getType() {
        return type;
    }
}

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

package com.huawei.it.external.enums;

import com.huawei.it.util.excetion.NoSuchEnumApplicationException;
import lombok.Data;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * 功能描述:变更作业打卡节点枚举
 *
 * @author YaoJiang-wx1047757
 * @since 2021-4-20
 */
public enum RfcClockNodeEnum {
    NONE("RFC_NONE",
            "未打卡",
            "RFC_SETOUT,RFC_ARRIVED",
            Arrays.asList(RfcOpStatusEnum.NOT_START)),
    SETOUT("RFC_SETOUT",
            "出发",
            "RFC_ARRIVED",
            Arrays.asList(RfcOpStatusEnum.NOT_START)),
    ARRIVED("RFC_ARRIVED",
            "到达",
            "RFC_START,RFC_VERIFY,RFC_ROLLBACK,RFC_LEAVE",
            Arrays.asList(RfcOpStatusEnum.NOT_START)),
    START("RFC_START",
            "开始",
            "RFC_VERIFY,RFC_ROLLBACK,RFC_COMPLETE_SUC,RFC_COMPLETE_FAIL",
            Arrays.asList(RfcOpStatusEnum.PREPARING)),
    VERIFY("RFC_VERIFY",
            "验证",
            "RFC_ROLLBACK,RFC_COMPLETE_SUC,RFC_COMPLETE_FAIL",
            Arrays.asList(RfcOpStatusEnum.PREPARING, RfcOpStatusEnum.IMPLEMENTING)),
    ROLLBACK("RFC_ROLLBACK",
            "倒回",
            "RFC_ROLLBACK_SUC,RFC_ROLLBACK_FAIL,RFC_COMPLETE_FAIL",
            Arrays.asList(RfcOpStatusEnum.PREPARING, RfcOpStatusEnum.IMPLEMENTING, RfcOpStatusEnum.VERIFYING)),
    ROLLBACK_SUC("RFC_ROLLBACK_SUC",
            "倒回成功",
            "RFC_COMPLETE_FAIL",
            Arrays.asList(RfcOpStatusEnum.ROLLBACKING)),
    ROLLBACK_FAIL("RFC_ROLLBACK_FAIL",
            "倒回失败",
            "RFC_COMPLETE_FAIL",
            Arrays.asList(RfcOpStatusEnum.ROLLBACKING)),
    COMPLETE_SUC("RFC_COMPLETE_SUC",
            "完成成功",
            "RFC_LEAVE",
            Arrays.asList(RfcOpStatusEnum.IMPLEMENTING, RfcOpStatusEnum.VERIFYING)),
    COMPLETE_FAIL(
            "RFC_COMPLETE_FAIL",
            "完成失败",
            "RFC_LEAVE",
            Arrays.asList(RfcOpStatusEnum.IMPLEMENTING, RfcOpStatusEnum.VERIFYING,
                    RfcOpStatusEnum.ROLLBACKING,RfcOpStatusEnum.ROLLBACK_SUCCESS, RfcOpStatusEnum.ROLLBACK_FAILED)),
    LEAVE("RFC_LEAVE",
            "离场",
            "",
            Arrays.asList(
                    RfcOpStatusEnum.CANCELLED, RfcOpStatusEnum.PREPARING,
                    RfcOpStatusEnum.COMPLETED_SUCCESS, RfcOpStatusEnum.COMPLETED_FAILED));

    // 打卡节点类型码
    private String code;

    // 打卡节点名称
    private String desc;

    // 下一个可打卡节点
    private String next;

    // 需要变更单状态
    private List<RfcOpStatusEnum> needStatus;

    RfcClockNodeEnum(String code, String desc, String next, List<RfcOpStatusEnum> needStatus) {
        this.code = code;
        this.desc = desc;
        this.next = next;
        this.needStatus = needStatus;
    }

    /**
     * 功能描述:获取当前枚举Code
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @return java.lang.String 枚举Code
     */
    public String getCode() {
        return code;
    }

    /**
     * 功能描述:获取当前枚举Desc
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @return java.lang.String 枚举Desc
     */
    public String getDesc() {
        return desc;
    }

    /**
     * 功能描述:获取当前枚举下一级枚举
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @return java.lang.String 下一级枚举
     */
    public String getNext() {
        return next;
    }

    /**
     * 功能描述:获取当前枚举所需的操作状态
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @return com.huawei.it.external.rfc.enums.RfcOprationStatusEnum 所需操作状态
     */
    public List<RfcOpStatusEnum> getNeedStatus() {
        return needStatus;
    }

    /**
     * 功能描述:根据枚举Code获取对应枚举
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @param code 指定枚举Code
     * @return com.huawei.it.external.enums.RfcClockNodeEnum 枚举
     * @throws NoSuchEnumApplicationException 无法找到指定Code的枚举时抛出异常
     */
    public static RfcClockNodeEnum byCode(String code) throws NoSuchEnumApplicationException {
        for (RfcClockNodeEnum value : RfcClockNodeEnum.values()) {
            if (value.getCode().equals(code)) {
                return value;
            }
        }
        throw new NoSuchEnumApplicationException("未找到对应的打卡类型");
    }

    /**
     * 功能描述:列举当前枚举的所有值
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-23
     * @return java.util.List<com.huawei.it.external.enums.RfcClockNodeEnum.RfcClockNode> RfcClockNode列表
     */
    public static List<RfcClockNode> map() {
        RfcClockNodeEnum[] values = RfcClockNodeEnum.values();
        List<RfcClockNode> resultList = new ArrayList(values.length);
        for (RfcClockNodeEnum value : values) {
            resultList.add(new RfcClockNode(value.code, value.desc, value.next));
        }
        return resultList;
    }

    /**
     * 功能描述:列举当前枚举的code和枚举实体的映射关系
     *
     * @author YaoJiang-wx1047757
     * @since 2021-05-08
     * @return java.util.Map<java.lang.String, com.huawei.it.external.enums.RfcClockNodeEnum> code和枚举实体的映射关系
     */
    public static Map<String, RfcClockNodeEnum> mapEnum() {
        Map<String, RfcClockNodeEnum> resultMap = new HashMap();
        for (RfcClockNodeEnum value : values()) {
            resultMap.put(value.getCode(), value);
        }
        return resultMap;
    }

    /**
     * 功能描述:列举当前枚举的code和desc的映射关系
     *
     * @author YaoJiang-wx1047757
     * @since 2021-05-08
     * @return java.util.Map<java.lang.String, java.lang.String> code和desc的映射关系
     */
    public static Map<String, String> mapDesc() {
        Map<String, String> resultMap = new HashMap();
        for (RfcClockNodeEnum value : values()) {
            resultMap.put(value.getCode(), value.getDesc());
        }
        return resultMap;
    }

    /**
     * 功能描述:根据当前code值获取下级列表
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-30
     * @param current 当前节点code值
     * @return java.util.List<com.huawei.it.external.enums.RfcClockNodeEnum.RfcClockNode> 下级列表
     * @throws NoSuchEnumApplicationException 未找到对应打卡节点时抛出异常
     */
    public static List<RfcClockNode> next(String current) throws NoSuchEnumApplicationException {
        RfcClockNodeEnum[] values = RfcClockNodeEnum.values();
        for (RfcClockNodeEnum value : values) {
            if (value.getCode().equals(current)) {
                String nextStr = value.getNext();
                if (!StringUtils.isEmpty(nextStr)) {
                    String[] nextNodeArr = nextStr.split(",");
                    List<RfcClockNode> resultList = new ArrayList(nextNodeArr.length);
                    for (String nextNode : nextNodeArr) {
                        RfcClockNodeEnum e = RfcClockNodeEnum.byCode(nextNode);
                        resultList.add(new RfcClockNode(e.code, e.desc, e.next));
                    }
                    return resultList;
                }
            }
        }
        throw new NoSuchEnumApplicationException("未找到对应的打卡类型");
    }

    /**
     * 功能描述:根据当前code值获取下级打卡节点枚举列表
     *
     * @author YaoJiang-wx1047757
     * @since 2021-05-12
     * @param current 当前节点code值
     * @return java.util.List<com.huawei.it.external.enums.RfcClockNodeEnum> 下级枚举列表
     * @throws NoSuchEnumApplicationException 未找到对应打卡节点时抛出异常
     */
    public static List<RfcClockNodeEnum> nextEnum(String current) throws NoSuchEnumApplicationException {
        Map<String, RfcClockNodeEnum> enumMap = RfcClockNodeEnum.mapEnum();
        for (Map.Entry<String, RfcClockNodeEnum> entry : enumMap.entrySet()) {
            RfcClockNodeEnum value = entry.getValue();
            if (value.getCode().equals(current)) {
                String nextStr = value.getNext();
                if (!StringUtils.isEmpty(nextStr)) {
                    String[] nextNodeArr = nextStr.split(",");
                    List<RfcClockNodeEnum> resultList = new ArrayList(nextNodeArr.length);
                    for (String nextNode : nextNodeArr) {
                        RfcClockNodeEnum e = enumMap.get(nextNode);
                        if (Objects.isNull(e)) {
                            throw new NoSuchEnumApplicationException("未找到对应的打卡类型");
                        }
                        resultList.add(e);
                    }
                    return resultList;
                }
            }
        }
        throw new NoSuchEnumApplicationException("未找到对应的打卡类型");
    }

    /**
     * 功能描述:变更打卡节点枚举的静态内部类,向其他端提供此枚举信息
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-23
     */
    @Data
    public static class RfcClockNode {
        private String nodeCode;
        private String nodeDesc;
        private List<String> nextNode;

        public RfcClockNode(String nodeCode, String nodeDesc, String nextCode) {
            this.nodeCode = nodeCode;
            this.nodeDesc = nodeDesc;
            if (!StringUtils.isEmpty(nextCode)) {
                String[] nextCodeArray = nextCode.split(",");
                this.nextNode = new ArrayList(nextCodeArray.length);
                for (String next : nextCodeArray) {
                    this.nextNode.add(next.trim());
                }
            }
        }
    }
}
/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved.
 */

package com.huawei.it.external.enums;

/**
 * 功能描述:变更操作单状态
 *
 * @author YaoJiang-wx1047757
 * @since 2021-04-22
 */
public enum RfcOpStatusEnum {
    NOT_START("Not Start", "未开始", true),
    PREPARING("Preparing", "准备中", true),
    IMPLEMENTING("Implementing", "实施中", true),
    VERIFYING("Verifying", "验证中", false),
    ROLLBACKING("Rollbacking", "倒回中", false),
    ROLLBACK_SUCCESS("Rollback Success", "倒回成功", false),
    ROLLBACK_FAILED("Rollback Failed", "倒回失败", false),
    COMPLETED_SUCCESS("Completed Success", "完成成功", false),
    COMPLETED_FAILED("Completed Failed", "完成失败", false),
    CANCELLED("Cancelled", "已取消", false);

    // 变更操作单状态值
    private String code;

    // 变更操作单描述
    private String desc;

    // 是否可取消
    private boolean canCancel;

    RfcOpStatusEnum(String code, String desc, boolean canCancel) {
        this.code = code;
        this.desc = desc;
        this.canCancel = canCancel;
    }

    /**
     * 功能描述:获取状态值
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @return java.lang.String 状态值
     */
    public String getCode() {
        return code;
    }

    /**
     * 功能描述:获取状态值描述信息
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @return java.lang.String 状态值描述信息
     */
    public String getDesc() {
        return desc;
    }

    /**
     * 功能描述:获取当前工单操作状态是否可取消
     *
     * @author YaoJiang-wx1047757
     * @since 2021-04-22
     * @return boolean 是否可取消
     */
    public boolean isCanCancel() {
        return canCancel;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值