供稿人:许庆洋
维护员工信息时,可以上传由配置 bropen.framework.osm.employee.signature.img.size 制定大小的手写签名图片,签名图片可以通过配置 p:opinion 标签的 signature 属性,显示在意见栏中,或者调用相应的API实现Word或WPS套打。
在下面的项目需求中,需要在表单的上方显示所有审批人的手写签名,并按照一定规则排序:
- 流程处于起草状态时,不显示签名;
- 如果流程被退回,则前面的签名都擦除;
- 按照分组显示意见;
- 如果任务列表中存在相同任务环节名,并且他们的任务实际办理人相同,且存在已完成的任务,则擦除已完成任务的环节签名;
- 签名显示分3类:审批环节的签名、审议环节的签名、总裁/董事长的签名:
 集团领导的审批任务,放到总裁、董事长审批行;
 关键任务列表,按时间顺序排列、根据审批人查重,放到审批行;
 其他任务列表,按照级别(员工title)排序、根据审批人查重,放到审议行;
- 每行最多显示8条签名,签名图片大小为100*40;
- 当为已办、或代理人打开待办、或者非任一人处理时,签名的标题处显示“代理”二字;
- 超时自动提交的任务签名处显示“系统提交”;
- 否决的任务签名处显示“否决”;
- 决定性意见为“不同意”的任务签名处显示“不同意”;
- 其他办理完毕的任务签名不存在,签名处则显示“已处理”。
签名栏显示效果如下图所示:

由于这是一个共性的需求,可以自定义一个显示手写签名栏的taglib标签,以下实现代码:
/**
 * 手写签名栏,放在表单的<h1>标签前
 * 例如:<boe:signature bean="${xxxxInstance}" />
 * @attr bean REQUIRED
 */
def signature = {attrs->
    if ( !attrs.bean?.id ) return;
    ProcessInstance flowInst = attrs.bean.workflowInstance;
    def proc = flowInst.processDefinition();
    // 流程处于起草状态时,不显示签名
    if ( flowInst.startNode() == flowInst.nodesName ) return;
    //
    List tasks = [], criticalNodes = [], otherNodes = [], leaderTasks, criticalTasks, otherTasks;
    // 需要显示签名的任务列表:非起草环节的待办
    Task lastDraftTask = flowInst.tasks[0];        // 计算最后一次起草环节的任务:签名从这里开始,一旦退回,签名擦除
    for ( t in flowInst.tasks ) if (t!=lastDraftTask && t.type==Task.TYPE_TODO && t.node==lastDraftTask.node && t.status!='terminated') lastDraftTask = t;
    for ( t in flowInst.tasks ) if (t.type==Task.TYPE_TODO && t.createTime>lastDraftTask.createTime && t.status!='terminated') tasks << t;
    // 如果退回到环节A,则不显示原来在环节A的任务-被退回来的环节A的任务(不含)
    // 例如路径 A-B-C-A,其中C-A是退回,则不显示前面三个任务(A-B-C)的签名
    tasks = CollectionUtils.sort(tasks, 'asc', 'id');
    for ( int i=0; i<tasks.size(); i++ ) {
        if ( !tasks[i] ) continue;
        String node = tasks[i].node;
        for ( int j=i+1; j<tasks.size(); j++ ) {
            if ( tasks[j].node==node && tasks[j].sendType==Task.SEND_TYPE_BACK ) {
                List prevs = tasks[j].prevAllTasks();
                for ( int k=i; k<j; k++ ) if ( prevs.contains(tasks[k]) ) tasks[k] = null;
                break;
            }
        }
    }
    // 如果任务列表中存在相同任务环节名,并且他们的任务实际办理人相同,且存在已完成的任务,则擦除已完成任务的环节签名
    for ( int i=0; i<tasks.size(); i++ ) {
        if ( tasks[i] == null || tasks[i].completeTime ) continue;
        for ( int j=0; j<i; j++ ) {
            if ( tasks[j] && tasks[j].completeTime && tasks[j].node==tasks[i].node && tasks[j].actorId==tasks[i].actorId ) tasks[j] = null;
        }
    }
    tasks -= null;
    // 集团领导的审批任务,放到总裁、董事长审批行
    Closure uniqueClosure = {it.node + "@" + it.actorId};
    Closure sortSwapper = { List valList, Date completeTime, Task task->
        return completeTime ?: task.createTime;
    }
    leaderTasks = CollectionUtils.findAll(tasks, "actorTitle", ["总裁","董事长"]);    // 后面再查重,下面两段代码还用这个list变量
    // 关键任务列表,按时间顺序排列、根据审批人查重,放到审批行
    for ( n in proc.values() ) if (n instanceof Map && n.node && n.critical ) criticalNodes << n.node;
    criticalTasks = CollectionUtils.sort(
            CollectionUtils.sort(CollectionUtils.findAll(tasks, "node", criticalNodes)-leaderTasks,
            "desc", "completeTime").unique(uniqueClosure), "asc", "completeTime", sortSwapper);
    // 其他任务列表,按照级别(员工title)排序、根据审批人查重,放到审议行
    for ( n in proc.values() ) if (n instanceof Map && n.node && !n.critical ) otherNodes << n.node;
    otherTasks = CollectionUtils.sort(
            CollectionUtils.findAll(tasks, "node", otherNodes)-leaderTasks, "desc",
            "completeTime", sortSwapper).unique(uniqueClosure);
    otherTasks = otherTasks.sort{-app.AppUtils.getLevelByTitle(it.actorTitle)};
    leaderTasks = leaderTasks.unique(uniqueClosure);    // 集团领导的审批任务查重
    // 渲染签名表格
    StringBuilder sb = new StringBuilder();
    sb << '<table style="padding: 5px 0; border-collapse: collapse;">'
    renderSignature("审批", criticalTasks, sb);
    renderSignature("审议", otherTasks, sb);
    renderSignature("总裁、董事长审批", leaderTasks, sb);
    sb << '</table>';
    out << sb
}
private renderSignature(String caption, List tasks, StringBuilder sb) {
    if ( !tasks ) return;
    sb << '<tr class="prop"><td class="nameX" style="white-space:normal; text-align:center; vertical-align:middle; width:100px" rowspan="'
    sb << (Math.ceil(tasks.size()/8) * 2) << '">' << caption << '</td>'
    // 每行最多显示8条签名,签名图片大小为100*40
    int max = 8;
    for ( int i=0; i<tasks.size(); i+=max ) {
        if ( i!=0 ) sb << '<tr class="prop">';
        renderSignature( tasks.subList(i,[i+max,tasks.size()].min()), sb );
        sb << '</tr>'
    }
}
private renderSignature(List tasks, StringBuilder sb) {
    for ( t in tasks ) {
        sb << '<td class="nameX" style="white-space:normal; text-align:center; vertical-align:middle; width:100px; padding:4px">'
        if ( t.actorSubstituteId ) {
            // 仅当为已办、或代理人打开待办、或者非任一人处理时,才显示“代理”二字
            if ( t.completeTime || t.substituteType==Substitute.TYPE_MOVE || t.actorSubstituteId==session.employeeId ) {
                sb << '(代理)<br/>';
            }
        }
        //sb << DateUtils.formatDatetime(t.createTime) << "<br/>"
        sb << bropen.framework.core.osm.Organization.splitName(t.organizationFullName,1) << '/' << (t.actorTitle?:"员工") << '<br/>' << t.actor << '</td>'
    }
    sb << '</tr><tr class="prop" style="height:40px">'
    for ( t in tasks ) {
        // 计算员工签名:超时自动提交的显示“系统提交”,签名不存在则显示“已处理”
        String sigid = null;
        if ( t.prevStatus == "timeout" ) {
            sigid = "wf_timeout"
        } else if ( t.decisiveOpinion == "不同意" ) {
            sigid = "wf_disagree";
        } else if ( t.transition == "否决" ) {
            sigid = "wf_reject";
        } else if ( t.actorSubstituteId ) {
            sigid = t.actorSubstitute?.isSignatureExists() ? t.actorSubstitute.id : "wf_done";
        } else {
            sigid = t.actor?.isSignatureExists() ? t.actor.id : "wf_done";
        }
        // 渲染单元格
        sb << '<td class="valueX" style="text-align:center; vertical-align:middle; padding:0px; width:100px">'
        if ( t.status=="completed" ) {
            sb << '<img width="100" height="40" src="' << createLink(controller:'app', action:'signatureImage', id:sigid) << '"/>'
        } else {
            sb << " "
        }
        sb << '</td>'
    }
}
上述代码中,显示签名图片的是一个app控制器的 signatureImage 操作,代码如下:
/**
 * 显示签名图片(不加水印),用于签名栏
 */
def signatureImage() {
    String filename = Employee.signatureFilename(params.id);
    File file = new File(filename);
    if ( file.exists() && file.canRead() ) {
        // 渲染图片,并设置1天的浏览器缓存
        osmEmployeeService.renderSignature( response, filename, false, 
            [validFor:86400, shared:true, store:true, lastModified:file.lastModified()] );
    } else render "";
}
此外,也可以直接使用产品中显示签名图片的控制器操作 osm/signatureImage,该操作将返回一个添加了干扰水印的签名图片。
定义完手写签名的taglib后,在form、edit页面上面添加foo:signature标签即可。代码如下:
<div class="body">
    <foo:signature bean="${xxxInstance}" />
    <h1>${entityName}</h1>
    ....
</div>
总之,根据需求的不同,我们可以很灵活的定义不同的规则,展示出不同显示效果的签名栏。