OpenJweb
功能开发实例
(简易审批流功能实现)
王保政
QQ:29803446
Email:baozhengw@netease.com
目
录
3.2
定义事务性计划表
(wf_work_plan)
的字段
...
6
3.5
编译后进入系统查看生成的列表页和编辑页
...
10
一、 业务需求描述
本文主要讲述如何使用
OpenJweb
快速开发平台快速定制开发一个简单的单据审批流程
,
本文采用某电厂事务性计划审批流程作为具体案例来讲解。
电厂的事务性计划每月由各部门的部门计划员填写,填写完毕后由部门计划员提交给分管厂长送审(计划检查人),分管厂长审批通过后,提交给厂计划员审核,厂计划员审核通过后提交给总经理审核,总经理审核通过后由厂计划员发布。其中厂计划员可以代理分管厂长审批计划。任一环节审核拒绝后都可以由计划填写人删除或修改。
二、 关于审批流
2.1 审批流配置
针对以上所述流程,一个事务性计划的具体审批流程见下表:
序
号
|
动作码
|
动作名称
|
前置状态位
|
后置状态位
|
业务描述
|
1
|
doCheck1
|
分管厂长审批
|
送审
(SendCheck)
|
分管厂长审核通过
(pass1)
|
部门计划员送审计划后
,
由分管厂长审批
|
2
|
proxyPass1
|
代理分管厂长审批
|
送审
(SendCheck)
|
分管厂长审核通过
(pass1)
|
当分管厂长因出差等原因无法登录系统,厂计划员可在页面上点代理分管厂长审批按钮
|
3
|
doCheck2
|
厂计划员审批
|
分管厂长审核通过
(pass1)
|
厂计划员审批通过
(pass2)
|
厂计划员审批分管厂长审批通过的计划
|
4
|
doManagerCheck
|
总经理审批
|
厂计划员审批通过
(pass2)
|
总经理审批通过
(pass3)
|
总经理审批厂计划员审批通过的计划
|
5
|
doPublish
|
计划发布
|
总经理审批通过
(pass3)
|
已发布(
Publish
)
|
厂计划员发布总经理审批通过的计划,发布后,所有人都可以看到这个计划
|
6
|
doReject1
|
分管厂长审核拒绝
|
送审
(SendCheck)
|
审核拒绝
(Reject)
|
在页面中公用一个审核拒绝按钮,由于系统中可以设置数据过滤器,不同角色进入系统会看到不同状态的数据,例如分管厂长进入系统中只能看到已发布和送审状态的数据,看不到正在由厂计划员或总经理审核的数据,所以不存在审批权混乱的问题。
|
7
|
doReject2
|
厂计划员审核拒绝
|
分管厂长审核通过
(pass1)
|
审核拒绝
(Reject)
|
|
8
|
doReject3
|
总经理审核拒绝
|
厂计划员审批通过
(pass2)
|
审核拒绝
(Reject)
|
下面是平台中配置好的审批流设置页面:
动作码:
什么是动作码?动作码是在页面中与动作按钮关联的一个标识,当用户在页面中点击一个按钮,页面会将这个动作码及所选记录的
ID
值传到后台的控制层来处理,例如分管厂长点了“分管厂长审核同意”按钮,页面将此按钮对应的状态码
doCheck1
和
ID
传到后台,后台根据动作码
doCheck1
和当前记录的当前审核状态为
SendCheck(
送审
)
做为查询条件,查到对应的后置状态码为
pass1(
分管厂长审核同意
)
,查到后将所选择的记录的审核状态设置为
pass1(
分管厂长审核通过
)
。下面是摘录了事务性计划列表页面的一段代码,其中
doCheck('doCheck1')
就是通过
javascript
调用将动作码提交给后台处理。
<authz:authorize ifAnyGranted="AUTH_BUTTON_PLAN_PASS1"><input type="button" name="cmdcheck" value="
同意
(
分管厂长
)" class="mybutton"
onClick="
doCheck('doCheck1')
">
</authz:authorize>
2.2 关于简易审批工作流的几个特点
简易审批工作流与 OA 工作流有很大的不同,简易审批工作流最常用的就是如上文所叙述的单据审批流,与 OA 工作流不同的是:
(1) 在一个流程节点中,由一个人审批后就可以更改单据的流程状态,不需要多人会签。
(2) 不需要启动一个流程实例也不需要按不同的人生成待办任务列表。简易审批流中,单据本身就可看做一个流程实例,待办任务列表就是按状态位过滤的单据条目。
(3) 关于简易的工作流的流程图,目前用 UML 的活动图和状态图都不直观,最直观的绘制方式遵循以下要点:
a) 矩形节点表示角色
b) 连线作为动作线而不是条件线(每一个动作线已定义了前置条件和后置条件)
c) 动作描述:动作描述按照角色 + 动作 + 结果这三个要素,例如:分管厂长审批通过。
三、 OpenJweb 平台开发示例
本章节讲述如何通过
OpenJweb
快速开发平台来实现事务性计划的维护和简易审批流,实际上,
如果只是作为数据增删改查的功能开发,通过
OpenJweb
平台定制一个功能可以完全不用写代码,只要在平台中定义了表结构,就可以利用平台自动创建数据库表、数据库表对应的
Java
实体类,及增删改查页面。这样功能的创建工作只要会计算机操作的人都可以去做,那么在具体的企业应用项目的实施中此平台带来的价值是什么?就是大大减少开发人员的人数和开发成本!
下面分析一下事务性计划这个业务对象所具有的字段,包括计划时间、计划内容、主办部门、计划检查人(即分管厂长)、审批状态、创建人、最后修改人等。当事务性计划的字段内容被确定后,我们就要将这些字段的相关信息录入到数据库中,具体操作过程
:
3.1 定义事务性计划表 (wf_work_plan)
实体类名必须以 org.apache.easframework.core.entity. 作为前缀,把表名 wf_work_plan 中的下划线去掉,并把下划线后的第一个字母大写就是此表的类名,如 wf_work_plan 对应的类名为 :WfWorkPlan, 所以实体类的全路径名就是 org.apache.easframework.core.entity.WfWorkPlan 。
3.2 定义事务性计划表 (wf_work_plan) 的字段
这个功能非常重要,因为此功能定义字段后作为建表的依据,而且还要在这里需要配置好每个表字段的页面属性,包括是否作为查询条件列、是否在编辑页面或列表页面中展示,以及在页面中的输入方式(如直接输入,日期选择,下拉列表, checkbox 等), JSP 代码生成器会依据这些属性来创建列表页面和编辑页面。下面是字段定义的界面:
上图是一个日期字段的定义,编辑页面输入方式为日期,当设置了输入方式为日期时,生成的 JSP 页面中对应的字段则能显示日期选择按钮,见下图:
下图定义了一个下拉列表字段,下拉列表既可以设置直接从数据字典中取值,也可以设置从某个表中取出名 / 值对:
这样在生成 Web 页面时,对应的字段就显示一个下拉列表。
关于预设默认值 :目前可以预设用户自定义的固定值,当前用户,当前用户所在部门,当前时间等,以后可以补充更多的默认值算法如按某格式的单据流水号生成器。
是否查询条件列选项 :当选中此项后,对应的字段会作为查询条件列显示在列表页面的查询条件下拉框中。见下图:
是否在列表页面中显示选项 :当选中此项后,在由平台生成 JSP 列表和编辑页面时,对应的字段会显示在列表页中,见下图的列表页面是通过平台自动生成的,列出的字段都是选中此选项后生成出来的。
是否在编辑页面中显示选项:
当选中此项后,平台自动生成的编辑页面中会显示此字段,并且字段的输入方式会按照预置的方式展示,如文本输入框,下拉选择框(自动从数据库获得下拉列表)、 CheckBox 选择框,日期选择框等。
3.3 在平台中创建数据库表
当所有字段定义完成后,打开表基本信息维护功能的页面,选择你刚才定义的表,然后点“生成数据库表”按钮即可以生成数据库表。这时进入数据库可以看到生成的数据库表。
系统除了创建数据库表外,还自动生成了一个 Java 实体类和 hibernate 映射文件,实体类的 java 文件名为 WfWorkPlan.java,hibernate 映射文件名为 WfWorkPlan.hbm.xml, 另外平台自动在 spring 的 datasource.xml 中增加了此 hibernate 映射文件的配置,见 datasource.xml 中部分配置:
<property name="mappingResources">
<list>
<value>org/apache/easframework/core/entity/EasWfActivity.hbm.xml</value>
……..
<!--OpenJWebGenerator-->
</list>
3.4 在平台中自动生成功能维护页面
当表创建完成后,在动态功能中选择功能菜单维护,新增一个功能配置:
编辑页面:
编辑页面注意选择你刚才创建的表,保存后在列表页面点“生成功能代码”,即可生成 JSP 页面代码,另外在 xwork.xml 中自动增加以下配置:
<action name="listEasWfActivity" class="org.apache.easframework.core.webwork.action.BaseAction">
<result name="input">/module/platform/editEasWfActivity.jsp</result>
<result name="success">/module/platform/listEasWfActivity.jsp</result>
<result name="select">/listEasWfActivity.action?operate=selectPageList</result>
<result name="edit">/module/platform/editEasWfActivity.jsp</result>
<result name="showList">/module/platform/listEasWfActivity.jsp</result>
<param name="serviceName">DBSupportService</param>
<param name="keyFieldName">objId</param>
<param name="entityClassName">org.apache.easframework.core.entity.EasWfActivity</param>
<param name="codeColumns"></param>
<param name="sortColumns"></param>
<param name="titleBar"> 系统管理 , 工作流 , 简易审批流设置 </param>
<param name="actionName">listEasWfActivity</param>
<param name="editTitle"> 简易审批流设置 </param>
</action>
由于平台生成了 java 实体类,所以到这一步运行 D:/easdev/build/userbuild.bat 后重新启动 tomcat, 再次进入系统。
3.5 编译后进入系统查看生成的列表页和编辑页
后,打开事务性计划的页面,这个页面是由平台创建出来的页面:
列表页(数据是后来录的,当然在刚开始建立的表中是没数据的,除了增删改之外,其他审批相关按钮是后来添加的):
通过平台生成的编辑页面 :
四、 权限体系
本平台的权限体系按照部门 - 人员 - 角色 - 权限集合的关系设置,权限的逻辑关系是柔性的,用户也可以直接在 acegi 的配置文件中配置权限检索的 SQL 。
4.1 首先维护组织机构和登录帐号
组织机构维护包括组织机构中的部门和人员的维护,人员属于一种组织结构类型,在组织结构维护中维护部门、人员及人员的登录口令:
配置登录帐号:
4.2 角色维护
按照当前事务性计划参与的角色新增部门计划员、分管厂长、厂计划员、总经理角色。
4.3 权限树维护及权限分配给角色
功能树左侧的 outlook 标签,树上的每个节点,及事务性计划页面上的每个按钮都配置了相应的权限码,在分配权限时可以选择权限节点分配给角色,下面是角色 - 权限分配界面:
(例如将分管厂长审批的按钮权限授权给分管厂长角色,其他角色看不到这个按钮),下图是将事务性计划维护页面的增加、删除、修改、送审、计划反馈维护功能按钮授权给部门计划员。
4.4 将已分配权限的角色授权给用户
将角色授权给用户后,用户可获得此角色拥有的所有权限。
下图是将部门计划员角色授权给测试人员:
4.5 按不同角色登录系统
分别按照部门计划员、分管厂长、厂计划员、总经理的帐号登录系统,不仅可以看到不同的功能树,而且不同的角色,同一个界面上的按钮也不相同,因为权限控制按照不同的角色显示或隐藏不同的按钮。
按部门计划员角色登录:
部门计划员只被授予计划管理模块,所以左侧功能树别的功能项已被隐藏,事务性计划页面只允许此角色使用新增、删除、修改、送审、计划反馈按钮,删除和修改也做了控制,不能删除和修改送审的计划。
按分管厂长登录,页面上显示不同的按钮:
下面是列表页面中的部分 JSP 代码,其中使用了 acegi 的权限标签来控制哪些按钮对当前角色可见:
<authz:authorize ifAnyGranted="AUTH_BUTTON_PLAN_ADD"><input type="button" name="cmdadd" value=" 新增 " onclick ="doAdd()" class="mybutton" ></authz:authorize>
<authz:authorize ifAnyGranted="AUTH_BUTTON_PLAN_EDIT"><input type="button" name="publish" value=" 修改 " class="mybutton" onClick=doEdit()></authz:authorize>
<authz:authorize ifAnyGranted="AUTH_BUTTON_PLAN_DEL">
<input type="button" name="cmdupdate" value=" 删除 " class="mybutton" onClick="doDelete()"></authz:authorize>
<authz:authorize ifAnyGranted="AUTH_BUTTON_PLAN_SENDCHECK"><input type="button" name="cmdcheck" value=" 送审 " class="mybutton" onClick="doSendCheck()">
</authz:authorize>
<authz:authorize ifAnyGranted="AUTH_BUTTON_PLAN_PASS3"><input type="button" name="cmdcheck" value=" 同意 ( 总经理 )" class="mybutton" onClick="doCheck('doManagerCheck')" >
</authz:authorize>
<authz:authorize ifAnyGranted="AUTH_BUTTON_PLAN_PASS1"><input type="button" name="cmdcheck" value=" 同意 ( 分管厂长 )" class="mybutton" onClick="doCheck('doCheck1')">
</authz:authorize>
<authz:authorize ifAnyGranted="AUTH_BUTTON_PLAN_PASS2"><input type="button" name="cmdcheck" value=" 同意 ( 厂计划员 )" class="mybutton" onClick="doCheck('doCheck2')">
</authz:authorize>
<authz:authorize ifAnyGranted="AUTH_BUTTON_PLAN_REJECT">
<input type="button" name="cmdcheck" value=" 审核拒绝 " class="mybutton" onClick="doReject()"></authz:authorize>
<authz:authorize ifAnyGranted="AUTH_BUTTON_PLAN_PUB">
<input type="button" name="cmdcheck" value=" 计划发布 " class="mybutton" onClick="doCheck('doPublish')"></authz:authorize>
<authz:authorize ifAnyGranted="AUTH_BUTTON_PLAN_FEED">
<input type="button" name="cmdcheck" value=" 计划反馈 " class="mybutton" "></authz:authorize>
五、 数据权限
在流程相关的功能开发中,同一表中检索出来的数据需要按不同的角色进行过滤,在事务性计划列表中,有以下业务规则:
(1) 部门计划员可以看到自己创建的计划、发布的计划,看不到别人创建的正在审批或者未提交的计划。
(2) 分管厂长只能看到推送给自己审批的计划和发布的计划。
(3) 厂计划员可以看到所有状态的计划。
(4) 总经理只能看到状态为厂计划员审核通过(等待总经理审批)的计划和已发布的计划。
实现按角色过滤数据的方式在计划列表被检索前,通过过滤器算法构造一个 SQL 表达式,在查询前附加到查询语句之后,这样可以实现按角色看到不同的计划,不过对于复杂的算法可能不能通过构造一个 SQL 的方式,也可以在数据集检索出来后在呈现到页面之前进行一次筛选,不过这样要重新计算分页,最好是能够构造一个 SQL 一次性查找出来。
过滤算法可以在一个实体类中实现,例如事务性计划的实体类为 WfWorkPlan.java 中增加了过滤算法,此算法返回一个 Hibernate 的 where 语句表达式 , 过滤器方法为:
public String getFilterExpress(String currLoginId,String defaultHql,String flag,String currRole) throws Exception
例如此方法中构造分管厂长的过滤表达式:
//2
分管厂长过滤
sql ="select count(*) from eas_roleorg_rel a,eas_login_user b,eas_roles c where a.org_id=b.obj_id and b.user_id='"+currLoginId+"' "
+" and a.role_id=c.obj_id
and c.role_name= '
分管厂长
'";
sCount = ServiceLocator.getDBSupportService().findSingleValueBySql(sql, null).toString();
if(Integer.parseInt(sCount)>0)
{
//
分管厂长只处理提交给本人的且单据状态为送审的计划
,
但可以查看已发布的计划
String sTmp="";
sTmp = " ((planCheckers = '"+currLoginId+"' and flowStatus='SendCheck') or flowStatus='Publish') ";
if(defaultHql.trim().length()>0)
{
sTmp = defaultHql + " and "+sTmp;
}
else
{
sTmp = defaultHql;
}
return sTmp;
}
需要注意 getFilterExpress 方法也要在 WfWorkPlan 的抽象类 AbstractEntity 中声明,这样在控制层中可以统一调用用抽象类的 getFilterExpress 。
六、 数据权限计算中的角色交叉问题
如果一个登录帐号既被授予了分管厂长,由被授予了厂计划员,甚至更多的角色,这时候改如何计算数据权限?这确实是一个难题,一帐号对应多角色在系统中是普遍存在的问题,这时如何过滤数据,可以考虑对需要按角色展示数据的时候,弹出一个角色选择框让用户单选其中的一个角色执行操作。
七、 控制层中的审批算法
以上是一个简易的计划审批流程在 OpenJWeb 平台中的开发过程,包括从建表到创建功能页面,功能页面创建出来后在列表页面加了一些流程审批相关的按钮 , 并在控制层的 BaseAction 中实现一个 doSendCheck 送审 ,doCheck(‘ 活动码 ’) 审核 , 和 doReject() 审核拒绝算法。
//
送审:可送审状态为空或拒绝的计划:
public
void
sendCheck()
throws
Exception
{
for
(
int
i=0;i<
this
.
selectedIds
.
length
;i++)
{
//
只有
flowStatus
状态为空或
''
或
Reject
才可以被送审
String userId
=
this
.getLoginUser();
String updateDt =
new
SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss"
).format(
new
Date(System.
currentTimeMillis
()));
String hql =
"update "
+
this
.
entityClassName
+
" vo set vo.flowStatus='SendCheck',vo.updateUid='"
+userId+
"',vo.updateDt='"
+updateDt+
"' where vo.objId='"
+
selectedIds
[i]+
"' and (vo.flowStatus='' or vo.flowStatus is null or vo.flowStatus='Reject')"
;
ServiceLocator.
getDBSupportService
().updateHql(hql,
null
);
}
this
.firstPage();
//
回到第一页
}
//
审核算法:
public
void
doCheck()
throws
Exception
{
//System.out.println("
从页面获得的动作标识:
");
//System.out.println(this.getActionFlag());
for
(
int
i=0;i<
this
.
selectedIds
.
length
;i++)
{
//
首先查出当前的状态
try
{
String statusCode = ServiceLocator.
getDBSupportService
().findSingleValueByHql(
"select flowStatus from "
+
this
.
entityClassName
+
" vo where vo.objId='"
+
this
.
selectedIds
[i]+
"'"
,
null
).toString();
//
从审批流设置表查找动作码为
this.actionFlag,
前置条件为当前状态的后置条件码
String hql =
""
;
hql =
"select resultStatus from EasWfActivity vo where vo.entityClassName='"
+
this
.
entityClassName
+
"' and vo.statusFieldName='flowStatus' and vo.actionCode='"
+
this
.getActionFlag()+
"' and vo.conditionStatus='"
+statusCode+
"'"
;
System.
out
.println(hql);
String nextStatus = ServiceLocator.
getDBSupportService
().findSingleValueByHql(hql,
null
).toString();
//
将状态改为审核同意
ServiceLocator.
getDBSupportService
().updateHql(
"update "
+
this
.
entityClassName
+
" set flowStatus='"
+nextStatus+
"',updateUid='"
+
this
.getLoginUser()+
"' where objId='"
+
this
.
selectedIds
[i]+
"'"
,
null
);
if
(nextStatus.equals(
"Publish"
))
{
AbstractEntity tmpEntity = (AbstractEntity)Class.
forName
(
this
.
entityClassName
).newInstance();
tmpEntity.afterPublish(
this
.
selectedIds
[i],
this
.getLoginUser());
}
}
catch
(Exception ex)
{
logger
.error(
"
用户
"
+
this
.getLoginUser()+
"
执行了不符合条件的审批
!"
);
}
}
this
.firstPage();
}
}
//
审核拒绝算法:
public
void
doReject()
throws
Exception
{
for
(
int
i=0;i<
this
.
selectedIds
.
length
;i++)
{
//
获取被选择列的流程状态位字段
String statusCode = ServiceLocator.
getDBSupportService
().findSingleValueByHql(
"select flowStatus from "
+
this
.
entityClassName
+
" vo where vo.objId='"
+
this
.
selectedIds
[i]+
"'"
,
null
).toString();
//
从审批流状态配置表查询有没有记录的前置状态为本记录的流程状态码且后置条件为
Reject
String sCount = ServiceLocator.
getDBSupportService
().findSingleValueByHql(
"select count(*) from EasWfActivity vo where vo.entityClassName='"
+
this
.
entityClassName
+
"' and vo.statusFieldName='flowStatus' and vo.conditionStatus='"
+statusCode+
"' and vo.resultStatus='Reject'"
,
null
).toString();
if
(Integer.
parseInt
(sCount)>0)
//
说明当前状态符合被设置为拒绝的条件
{
ServiceLocator.
getDBSupportService
().updateHql(
"update "
+
this
.
entityClassName
+
" set flowStatus='Reject',updateUid='"
+
this
.getLoginUser()+
"' where objId='"
+
this
.
selectedIds
[i]+
"'"
,
null
);
}
}
this
.firstPage();
//
动作执行完后返回到第一个页面
}