1. 工作流开发节点(Since:V5.6)
工作流开发节点有别于系统的标准节点,
我们的标准节点是静态的,可以指定人员、部门、岗位、职务级别、组、角色等。
而开发节点可以执行一些特定的动作,比如创建人员、解锁账号、创建邮箱、发送邮件等,目前开发节点只对表单流程开放。
它比流程事件接口更进一步,将“线”上执行的操作显性表示在“节点”之上。
以下面两个场景为例,绿色的部分都是流程图上的开发节点。
结合表单,主管和HR审批完毕以后,可以利用单据中的人员姓名、部门、岗位、职务级别等信息,补充登录名,通过创建协同账号节点自动创建人员,而无需经过单位管理员。 同时可通过邮件或短信节点发送Offer,创建企业邮箱帐号、VPN账号等。
订票节点与上面场景的主要区别是增加了一个等待节点(接收订票信息),这个节点可以通过外部调用REST服务激活处理。
1.1. 跨单位人员调动
价值:
以跨单位人员调动为例,目前的步骤是
- A单位单位管理员登录,调出人员。
- 集团管理员登录,分配人员。
基于开发节点,我们可以把它调整为:
- A单位HR申请调出人员,填写调往单位。
- A单位相关主管审批。
- B单位HR补充部门、岗位和职务级别等信息。
- B单位相关主管审批。
- 自动更新人员信息,完成调动。
这样调整将调动职能由IT人员转向业务人员,而且能够使用流程审批。
1.1.1. 场景
- 部门主管选择调出人员;填写调往单位和调往部门。
- 接收单位部门主管补充岗位和职务级别,处理提交。
- 在跨单位人员节点,自动将指定人员调动到调往单位,并更新部门、岗位、职务级别信息。
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 表单管理员设置流程,添加订票节点。
订票节点需设为阻塞模式,这样节点不被处理之前,无法进入后续节点。
2 出差人员填写机票预订申请表。
航班部分可以做一个表单自定义控件,弹出携程(腾邦)的页面,选择航班并回填。
3 主管审批后,进入订票节点。
订票节点将乘机人信息发送到第三方票务系统,等待出票响应。
4 第三方票务系统出票后回调流程REST接口,回写出票信息,处理节点。
回调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代码,执行需要的操作。
配置脚本
这里写了两句简单的Groovy语句(当然,您也可以写纯Java代码,前面的语句等同于System.out.println),首先,执行时在控制台输出信息,然后返回执行的状态。
最终执行的结果可以在节点熟悉的动作执行情况中看到。
节点动作代码 *
脚本节点参考实现
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接口,完成了通过表单流程创建人员的功能。
流程中的创建人员、发送Offer、创建企业邮箱、创建VPN账号、发送新员工入职须知节点,全都是脚本节点,可根据客户实际环境进行编码。
在创建人员脚本节点中录入下面的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
如下所示
1.5. 示例
概括起来,动作节点支持以下特性
- 节点动作:开发人员可以编写节点的执行逻辑。
- 节点配置:开发人员抽象不变部分为节点动作的执行逻辑,提炼可变参数给最终用户配置,提高节点的可扩展性。
- 节点执行模式:阻塞,不阻塞
- 容错模式:忽略所有错误,等待人工干预
- 节点名称
- 执行情况反馈:是否执行成功、执行时间、执行时的提示信息
- 处理开发节点的REST接口
脚本节点(BaseSuperNodeAction.getNodeId) A流程中添加的脚本节点(activityId)流转中的脚本节点(token)
示例代码和相关表单请从此处下载。