1. 工作流开发节点(Since:V5.6)

工作流开发节点有别于系统的标准节点,

我们的标准节点是静态的,可以指定人员、部门、岗位、职务级别、组、角色等。

而开发节点可以执行一些特定的动作,比如创建人员、解锁账号、创建邮箱、发送邮件等,目前开发节点只对表单流程开放。

它比流程事件接口更进一步,将“线”上执行的操作显性表示在“节点”之上。

以下面两个场景为例,绿色的部分都是流程图上的开发节点。

img

结合表单,主管和HR审批完毕以后,可以利用单据中的人员姓名、部门、岗位、职务级别等信息,补充登录名,通过创建协同账号节点自动创建人员,而无需经过单位管理员。 同时可通过邮件或短信节点发送Offer,创建企业邮箱帐号、VPN账号等。

img

订票节点与上面场景的主要区别是增加了一个等待节点(接收订票信息),这个节点可以通过外部调用REST服务激活处理。

1.1. 跨单位人员调动

价值:

以跨单位人员调动为例,目前的步骤是

  • A单位单位管理员登录,调出人员。
  • 集团管理员登录,分配人员。

基于开发节点,我们可以把它调整为:

  • A单位HR申请调出人员,填写调往单位。
  • A单位相关主管审批。
  • B单位HR补充部门、岗位和职务级别等信息。
  • B单位相关主管审批。
  • 自动更新人员信息,完成调动。

这样调整将调动职能由IT人员转向业务人员,而且能够使用流程审批。

1.1.1. 场景

  • 部门主管选择调出人员;填写调往单位和调往部门。
  • 接收单位部门主管补充岗位和职务级别,处理提交。
  • 在跨单位人员节点,自动将指定人员调动到调往单位,并更新部门、岗位、职务级别信息。

img

img

1.1.2. 节点动作代码

package com.seeyon.ctp.workflow.devnode;

import java.util.Map;
import java.util.UUID;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.seeyon.ctp.common.AppContext;
import com.seeyon.ctp.organization.bo.V3xOrgMember;
import com.seeyon.ctp.organization.manager.OrgManager;
import com.seeyon.ctp.organization.manager.OrgManagerDirect;
import com.seeyon.ctp.workflow.supernode.BaseSuperNodeAction;
import com.seeyon.ctp.workflow.supernode.SuperNodeResponse;

/**
 * 跨单位调动人员节点。
 * 
 * @author 朱二阳
 * 
 */
public class MoveMemberNodeAction extends BaseSuperNodeAction {
    private static final Log logger = LogFactory
            .getLog(MoveMemberNodeAction.class);

    /**
     * 节点动作撤销。
     */
    public void cancelAction(String token, String activityId) {

    }

    /**
     * 确认节点动作执行。
     */
    public SuperNodeResponse confirmAction(String token, String activityId,
            Map params) {
        return this.executeAction(token, activityId, params);
    }

    /**
     * 动作执行方法。
     * 
     * @param token
     *            外部调用节点的依据。
     * @param activityId
     *            节点实例的唯一标识。
     * @param params
     *            节点执行的上下文信息,通过CTP_FORM_DATA key可以获取表单数据。
     * @return 执行结果。
     */
    public SuperNodeResponse executeAction(String token, String activityId,
            Map params) {
        SuperNodeResponse response = new SuperNodeResponse();
        OrgManager orgManager = (OrgManager) AppContext.getBean("orgManager");
        OrgManagerDirect orgManagerDirect = (OrgManagerDirect) AppContext
                .getBean("orgManagerDirect");
        Map data = getFormData(params);
        try {
            V3xOrgMember member = orgManager.getMemberById(Long.parseLong(data
                    .get("调出人员").toString()));
            member.setOrgAccountId(Long.parseLong(data.get("调往单位").toString()));
            member.setOrgDepartmentId(Long.parseLong(data.get("调往部门")
                    .toString()));
            member.setOrgPostId(Long.parseLong(data.get("岗位").toString()));
            member.setOrgLevelId(Long.parseLong(data.get("职务级别").toString()));
            orgManagerDirect.updateMember(member);
            response.setDataId(String.valueOf(UUID.randomUUID()
                    .getMostSignificantBits()));
            response.setReturnCode(1);
            response.setReturnMsg("跨单位调动人员节点处理成功");

        } catch (Throwable e) {
            logger.info("跨单位调动人员失败", e);
            response.setReturnCode(0);
            response.setReturnMsg("跨单位调动人员节点处理失败");
        }

        return null;
    }

    private Map getFormData(Map params) {
        return (Map) params.get("CTP_FORM_DATA");
    }

    /**
     * 节点标识。
     */
    public String getNodeId() {
        return "dev_node_move";
    }

    /*
     * 节点名称。
     */
    public String getNodeName() {
        return "跨单位调动人员";
    }

    /**
     * 选择时的显示顺序。
     */
    public int getOrder() {
        return 4;
    }

}

1.2. 订票

1.2.1. 场景

1 表单管理员设置流程,添加订票节点。

img

订票节点需设为阻塞模式,这样节点不被处理之前,无法进入后续节点。

img

2 出差人员填写机票预订申请表。

航班部分可以做一个表单自定义控件,弹出携程(腾邦)的页面,选择航班并回填。

img

3 主管审批后,进入订票节点。

订票节点将乘机人信息发送到第三方票务系统,等待出票响应。

img

4 第三方票务系统出票后回调流程REST接口,回写出票信息,处理节点。

img

回调REST接口为flow/notification/{flowToken},示例如下

CTPRestClient client = clientManager.getRestClient();
client.authenticate(userName, password);

client.post("flow/notification/" + "-4550005961272964826",
    new HashMap() {
        {
            put("message",
                "马先森/12月21日/CZ3903/北京首都机场(T2)17:10-成都双流国际机场(T2)20:10/已出票/请准时登机/祝您旅途愉快!");
            put("returnCode", 1);
        }
    }, Integer.class);

1.3. 节点动作代码

package com.seeyon.ctp.workflow.devnode;

import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.seeyon.ctp.workflow.supernode.BaseSuperNodeAction;
import com.seeyon.ctp.workflow.supernode.SuperNodeResponse;
/**
 * 工作流订票节点。
 * 
 * @author wayne
 * 
 */
public class TicketNodeAction extends BaseSuperNodeAction {

    private static final Log logger = LogFactory.getLog(TicketNodeAction.class);

    public int getOrder() {
        return 1;
    }

    public String getNodeId() {
        return "dev_node_ticket";
    }

    public String getNodeName() {
        return "订票节点";
    }

    public SuperNodeResponse executeAction(String token, String activityId,
            Map params) {

        SuperNodeResponse response = new SuperNodeResponse();
        response.setReturnCode(0);
        response.setReturnMsg("等待确认:" + token);
        // TODO 调用机票服务(如携程、腾邦),发送乘客姓名、身份证、起点、目的地、航班信息。
        // 将token同时发送给第三方,等待回调。
        System.out.println("等待确认:" + token);
        return response;
    }

    public void cancelAction(String token, String activityId) {
        logger.info("撤销订票:" + token);
    }

    public SuperNodeResponse confirmAction(String token, String activityId,
            Map params) {

        SuperNodeResponse response = new SuperNodeResponse();
        response.setReturnCode(0);
        response.setReturnMsg("等待确认:" + token);
        // TODO 调用机票服务(如携程、腾邦),发送乘客姓名、身份证、起点、目的地、航班信息。
        // 将token同时发送给第三方,等待回调。
        System.out.println("等待确认:" + token);
        return response;
    }

    public void cancelAction(String token, String activityId) {
        logger.info("撤销订票:" + token);
    }

    public SuperNodeResponse confirmAction(String token, String activityId,
            Map params) {
        return this.executeAction(token, activityId, params);
    }

}

1.4. 脚本节点

1.4.1. 场景

这是一个更进一步的示例,定义了一种通用可扩展的节点,管理员可以在节点的配置页面编写Groovy或Java代码,执行需要的操作。

img

配置脚本

img

这里写了两句简单的Groovy语句(当然,您也可以写纯Java代码,前面的语句等同于System.out.println),首先,执行时在控制台输出信息,然后返回执行的状态。

img

最终执行的结果可以在节点熟悉的动作执行情况中看到。

img

节点动作代码 *

脚本节点参考实现

package com.seeyon.ctp.workflow.devnode;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.seeyon.ctp.common.script.ScriptEvaluator;
import com.seeyon.ctp.workflow.devnode.manager.GroovyNodeManager;
import com.seeyon.ctp.workflow.supernode.BaseSuperNodeAction;
import com.seeyon.ctp.workflow.supernode.SuperNodeActionUrl;
import com.seeyon.ctp.workflow.supernode.SuperNodeResponse;

/**
 * 工作流脚本节点
 * 
 * @author wayne
 * 
 */
public class GroovyScriptNodeAction extends BaseSuperNodeAction {

    private static final Log logger = LogFactory
            .getLog(GroovyScriptNodeAction.class);
    private GroovyNodeManager groovyNodeManager;

    public GroovyNodeManager getGroovyNodeManager() {
        return groovyNodeManager;
    }
    public void setGroovyNodeManager(GroovyNodeManager groovyNodeManager) {
        this.groovyNodeManager = groovyNodeManager;
    }

    public int getOrder() {
        return 2;
    }

    public String getNodeId() {
        return "dev_node_groovy";
    }

    public String getNodeName() {
        return "脚本节点";
    }

    public SuperNodeResponse executeAction(String token, String activityId,
            Map params) {

        SuperNodeResponse response = new SuperNodeResponse();
        try {
            Map context = new HashMap();
            context.putAll(params);
            StringBuilder script = new StringBuilder(
                    "import static com.seeyon.ctp.common.constdef.ConstDefUtil.getConstDefValue as _;");
            script.append(groovyNodeManager.getScript(activityId));
            Object result = ScriptEvaluator.getInstance().eval(
                    script.toString(), context);
            Thread.sleep(10000);
            response.setDataId(String.valueOf(UUID.randomUUID()
                    .getMostSignificantBits()));
            response.setReturnCode(1);
            response.setReturnMsg("执行成功:" + result);
        } catch (Throwable e) {
            logger.error("脚本执行失败:" + e.getLocalizedMessage(), e);
            response.setReturnCode(0);
            response.setReturnMsg("脚本执行失败:" + e.getLocalizedMessage());
        }

            return response;
    }

    public void cancelAction(String token, String arg1) {
        logger.info("撤销脚本执行!");
    }

    public SuperNodeResponse confirmAction(String token, String activityId,
            Map params) {
        return this.executeAction(token, activityId, params);
    }

    /**
     * 动作配置信息查看页面,流转过程中查看节点信息。
     */
    public SuperNodeActionUrl getActionConfigDisplayUrl(String token,
            final String activityId) {
        // 偷懒了,管理员配置界面应该有别于普通用户的查看界面
        // 这样做的后果就是流转过程中也可以改脚本
        return new SuperNodeActionUrl() {

            @Override
            public String getUrl() {
                return "/workflow/groovyNodeController.do?method=edit&activityId="
                        + activityId;
            }

            @Override
            public int getWidth() {
                return 800;
            }

            @Override
            public int getHeight() {
                return 600;
            }

            @Override
            public String getTitle() {
                return "配置脚本节点";
            }
        };
    }

    /**
     * 动作配置信息编辑页面,表单管理员定义表单时可进行设置。
     */
    public SuperNodeActionUrl getActionConfigUrl(String activityId) {
        return this.getActionConfigDisplayUrl("", activityId);
    }
}

配置页面的jsp

<%@ page contentType="text/html; charset=UTF-8" isELIgnored="false"%>
<%@ include file="/WEB-INF/jsp/common/common.jsp"%>
<html class="h100b">
 <head> 
  <title>Configuration</title> 
 </head>
 <body class="h100b"> 
  <form id="form" method="POST" action="groovyNodeController.do?method=save"> 
   <label>节点名称:</label>
   <br />
   <input id="name" name="name" value="${name}" />
   <br /> 
   <input type="hidden" name="activityId" value="${activityId}" /> 
   <label>脚本:</label>
   <br /> 
   <textarea name="script" rows="30" cols="120">${script}</textarea> 
  </form>  
  <script>
  function OK(){
    var name = $('#name').val();
    $('#form').submit();
    return [name]; // 返回的数组中0号元素为节点的名称,就是在这里,把“脚本节点”改成了“创建人员”
  }
  </script> 
 </body>
</html>

这个示例的Manager没有对脚本进行持久化,都是暂时保存在内存中,重启就得重新设置。

1.4.2. 利用脚本节点创建人员

下面的代码利用脚本动作节点和REST接口,完成了通过表单流程创建人员的功能。

img

流程中的创建人员、发送Offer、创建企业邮箱、创建VPN账号、发送新员工入职须知节点,全都是脚本节点,可根据客户实际环境进行编码。

img

在创建人员脚本节点中录入下面的Groovy代码,注意,这个示例只做教学用途,如果要实际使用还需要根据实际场景定制表单并添加容错逻辑。

def data = [
    'name':CTP_FORM_DATA['姓名'],
    'loginName' : CTP_FORM_DATA['登录名'],
    'orgAccountId' : CTP_FORM_DATA['单位'],
    'orgDepartmentId' : CTP_FORM_DATA['部门'],
    'orgPostId' : CTP_FORM_DATA['岗位'],
    'orgLevelId' : CTP_FORM_DATA['职务级别'],
    'code' : CTP_FORM_DATA['编号']
]

def clientManager = com.seeyon.client.CTPServiceClientManager.getInstance('http://127.0.0.1')
def client = clientManager.getRestClient()
client.authenticate('rest','123456')
def result = client.post('orgMember',data,String.class)
print result
return result

如下所示

img

img

1.5. 示例

概括起来,动作节点支持以下特性

  • 节点动作:开发人员可以编写节点的执行逻辑。
  • 节点配置:开发人员抽象不变部分为节点动作的执行逻辑,提炼可变参数给最终用户配置,提高节点的可扩展性。
  • 节点执行模式:阻塞,不阻塞
  • 容错模式:忽略所有错误,等待人工干预
  • 节点名称
  • 执行情况反馈:是否执行成功、执行时间、执行时的提示信息
  • 处理开发节点的REST接口

脚本节点(BaseSuperNodeAction.getNodeId) A流程中添加的脚本节点(activityId)流转中的脚本节点(token)

示例代码和相关表单请从此处下载

results matching ""

    No results matching ""