jbpm的智能选择审批人及人工任务分配的实现思路

简介:

一、jbpm中审批人的概念


在工作流中每一个人工任务的办理者被称作审批人,在原生的jbpm定义中,我们可以看到assignee和assignmentHandler这两个标签。

1
2
3
4
5
6
7
8
< task  name = "review"  g = "96,16,127,52" >  
     < assignment-handler  class = "org.jbpm.examples.task.assignmenthandler.AssignTask" >  
       < field  name = "assignee" >  
         < string  value = "chao.gao"  />  
       </ field >  
     </ assignment-handler >  
     < transition  to = "wait"  />  
   </ task >

assignee如果是单用户,可以是字符串;如果是多用户,则是列表或数组;另外也可以使用类似jsp中的正则表达式,进行动态审批人的指定

如果有assignee,而没有分配处理器标签,则引擎自动将该人工任务分配给所有assignee。如有assignmentHandler会按照默认或定制的分配处理器制定的规则进行审批人的智能筛选以及对任务进行动态的指派。

在会签人方案一文中,提到了关于使用assignmentHandler生成会签子任务的实例,在决策里也提到了DefaultDecisionHandler,jbpm使用监听器模式对不同的处理类进行调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**  
      * @author Tom Baeyens  
      */  
     public  class  AssignTask  implements  AssignmentHandler {   
          
       private  static  final  long  serialVersionUID = 1L;   
       
       String assignee;   
       
       public  void  assign(Assignable assignable, OpenExecution execution) {   
         assignable.setAssignee(assignee);   
       }   
     }

以上是jbpm审批人的原生概念以及如何将指定或动态指定的用户与人工任务相绑定的理论知识。

下面阐述我们的审批人选择以及分配任务的思路

二、智能选择审批人

  审批人设置表结构

wKioL1TcQijD8TvnAAIRX6WIKV4987.jpg


    数据列表如下

wKiom1TcQj2y4m74AAUDqSVCszA605.jpg

  页面设置:

wKiom1TcXYPicv0YAAF46tJqBao488.jpg

一般情况下,商用工作流都提供基于组织架构、汇报关系、角色的审批人选择方法,OA中也提供了相应功能。基于组织架构上下级关系,主要有当前部门、当前部门之上级部门、当前部门上级部门之上级部门,基于组织架构的层级关系,主要有一级部门、二级部门、三级部门、子公司等。以上两种规则,除当前部门外还可以通过表单数据指定部门的编码。基于组织架构的成员,可以将本架构内所有成员全部纳入审批人列表。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
private  Set<String> getAssigneeByAssignSetting(FlowRunTime flowRunTimeInfo,
             UserAssignEntity assignSetting) {
         Set<String> candidateUserIdList =  new  HashSet<String>();
 
         if ( "Y" .equals(assignSetting.getIsForm())){
             String fieldValue = getFiledValueByAssignSetting(assignSetting, flowRunTimeInfo);
             if (StringUtils.isNotEmpty(fieldValue) && assignSetting.getFieldName().contains( "," )){
                 String[] staffIds = fieldValue.split( "[,]" );
                 for (String staffId : staffIds){
                     if (StringUtils.isNotEmpty(staffId)){
                         StaffinfoEntity staffinfoEntity = staffInfoService.queryStaffInfoByStaffId(staffId);
                         if (staffinfoEntity !=  null  && StringUtils.isNotEmpty(staffinfoEntity.getStaffCode())){
                             candidateUserIdList.add(staffinfoEntity.getStaffCode());
                         } else  {
                             throw  new  UserAssignException( "通过表单选人出现错误,请检查!" );
                         }
                     }
                 }
             } else   {
                 StaffinfoEntity staffinfoEntity = staffInfoService.queryStaffInfoByStaffId(fieldValue);
                 if (StringUtils.isEmpty(fieldValue)){
                     throw  new  UserAssignException( "通过表单选人出现错误,请检查!" );
                 }
                  OrgEntity org = orgService.queryOrgByCode(fieldValue);
                  if (! "ShanghaiHQ" .equals(fieldValue)){
                      List<HrbpEntity> hrbpEntityList = hrbpService.queryHrbpByDqCode(fieldValue);
                      if (!CollectionUtils.isEmpty(hrbpEntityList)){
                          for (HrbpEntity hrbp : hrbpEntityList){
                              String staffCode = staffInfoService.queryStaffByStaffId(hrbp.getHrbpId()).getStaffCode();
                              if (StringUtils.isNotEmpty(staffCode) && !candidateUserIdList.contains(staffCode)){
                                  candidateUserIdList.add(staffCode);
                              }                       
                          }
                      }
                  }
                 if (staffinfoEntity !=  null ){
                     if  (UserAssignConstants.USERASSIGN_LEADER_OF_LASTSTEP.equals(assignSetting.getChooseRule()) || 
                         UserAssignConstants.USERASSIGN_LEADER_OF_STARTASSIGNEE.equals(assignSetting.getChooseRule())) {
                         String directleaderCode = staffInfoService.queryLeader(fieldValue).getDirectLeaderCode();
                         if (StringUtils.isNotEmpty(directleaderCode)){
                         StaffinfoEntity directLeader = staffInfoService.queryStaffInfoByStaffId(directleaderCode);
                             if  (directLeader !=  null ) {
                                 candidateUserIdList.add(directLeader.getStaffCode());  
                             }
                        
                     } else  if (UserAssignConstants.USERASSIGN_LEADER_OF_LEADER.equals(assignSetting.getChooseRule())){
                         String indirectleaderCode = staffInfoService.queryLeader(fieldValue).getLeapfrogLeaderCode();  
                         StaffinfoEntity indirectLeader = staffInfoService.queryStaffInfoByStaffId(indirectleaderCode);
                         if (indirectLeader !=  null ){
                             candidateUserIdList.add(indirectLeader.getStaffCode());
                         }
                     else  if (UserAssignConstants.USERASSIGN_LEADER_OF_LEADER_OF_LEADER.equals(assignSetting.getChooseRule())){
                         String indirectleaderCode = staffInfoService.queryLeader(fieldValue).getLeapfrogLeaderCode();   //越级主管
                         if (StringUtils.isNotEmpty(indirectleaderCode)){
                             StaffinfoEntity indirectLeader = staffInfoService.queryStaffInfoByStaffId(indirectleaderCode);
                             if (indirectLeader !=  null ){
                                 String inIndirectLeaderStaffId = staffInfoService.queryLeader(indirectLeader.getStaffId()).getDirectLeaderCode();
                                 if (StringUtils.isNotEmpty(inIndirectLeaderStaffId)){
                                     StaffinfoEntity inIndirectLeader = staffInfoService.queryStaffInfoByStaffId(inIndirectLeaderStaffId);
                                     if (inIndirectLeader !=  null ){
                                         candidateUserIdList.add(inIndirectLeader.getStaffCode());
                                     }
                                 }
                             }
                         }
                     }
                     if (UserAssignConstants.USERASSIGN_STARTER.equals(assignSetting.getChooseRule())){
                             candidateUserIdList.add(staffinfoEntity.getStaffCode());
                     }
       
                 }
                if (org !=  null ){
                     if (UserAssignConstants.USERASSIGN_LEADER_OF_ORG.equals(assignSetting.getChooseRule())){ //当前部门的领导:
                         if  (org !=  null  && !StringUtils.isBlank(org.getManagerCode())) {
                             String managerCode = org.getManagerCode();
                             candidateUserIdList.add(managerCode);
                         }
                     else  if (UserAssignConstants.USERASSIGN_LEADER_OF_LEADER_OF_ORG.equals(assignSetting.getChooseRule())){ //申请部门的上级领导:
                         if  (org !=  null  && !StringUtils.isBlank(org.getParentCode())) {
                             OrgEntity orgParent  = orgService.queryOrgByCode(org.getParentCode());
                             if  (orgParent!= null  && !StringUtils.isBlank(orgParent.getManagerCode())) {
                                 String managerCode = orgParent.getManagerCode();
                                 candidateUserIdList.add(managerCode);
                             }
                         }
                     else  if (UserAssignConstants.USERASSIGN_LEADER_OF_LEADER_OF_LEADER_OF_ORG.equals(assignSetting.getChooseRule())){ //申请部门的上级之上级领导:
                          if  (org !=  null  && !StringUtils.isBlank(org.getParentCode())) {
                              OrgEntity orgtwo  = orgService.queryOrgByCode(org.getParentCode());
                              if  (orgtwo!= null  && !StringUtils.isBlank(orgtwo.getManagerCode())) {
                                  OrgEntity orgParent = orgService.queryOrgByCode(orgtwo.getParentCode());
                                  if  (orgParent!= null  && !StringUtils.isBlank(orgParent.getManagerCode())) {
                                      String managerCode = orgParent.getManagerCode();
                                      candidateUserIdList.add(managerCode);
                                 }
                              }
                          }
                     else  if (UserAssignConstants.USERASSIGN_LEADER_OF_PLATFORM_OR_COMPANY.equals(assignSetting.getChooseRule())){ //申请部门的子公司领导:子公司领导
                         OrgEntity orgPlatform = orgService.queryPlatformEntityByDepCode(fieldValue);
                         if  (orgPlatform !=  null  && !StringUtils.isBlank(orgPlatform.getManagerCode())) {
                             String managerCode = orgPlatform.getManagerCode();
                             candidateUserIdList.add(managerCode);
                         }
                         
                     else  if (UserAssignConstants.USERASSIGN_LEADER_OF_THIRD_ORG.equals(assignSetting.getChooseRule())){ //三级部门
                         
                         if  (OrgConstants.LEVEL_THIRD_ORG.equals(org.getBranchOrg())) {
                             if  (StringUtils.isNotEmpty(org.getManagerCode())) {
                                 String managerCode = org.getManagerCode();
                                 candidateUserIdList.add(managerCode);
                             }
                         else  if (OrgConstants.LEVEL_SECOND_ORG.equals(org.getBranchOrg()) || OrgConstants.LEVEL_FIRST_ORG.equals(org.getBranchOrg()) 
                                 || OrgConstants.LEVEL_PLATFORM.equals(org.getBranchOrg())){
 
                             if (StringUtils.isNotEmpty(org.getManagerCode())){
                                 candidateUserIdList.add(org.getManagerCode());
                             }
                             
                         } //如果是一级部门却要找三级部门,只能提示找不到;
                    
                     else  if (UserAssignConstants.USERASSIGN_LEADER_OF_SECOND_ORG.equals(assignSetting.getChooseRule())){ //二级部门
                             if (OrgConstants.LEVEL_SECOND_ORG.equals(org.getBranchOrg())){
                                 String managerCode = org.getManagerCode();
                                 if (!StringUtils.isBlank(managerCode)){
                                     candidateUserIdList.add(managerCode);
                                 }
                             } else  if (OrgConstants.LEVEL_THIRD_ORG.equals(org.getBranchOrg())){
                                 if  (!StringUtils.isBlank(org.getParentCode())) {
                                     OrgEntity orgParent  = orgService.queryOrgByCode(org.getParentCode());
                                    if  (orgParent!= null  && !StringUtils.isBlank(orgParent.getManagerCode())) {
                                        String managerCode = orgParent.getManagerCode();
                                        candidateUserIdList.add(managerCode);
                                      }
                                  }
                             else  if (OrgConstants.LEVEL_FIRST_ORG.equals(org.getBranchOrg()) || OrgConstants.LEVEL_PLATFORM.equals(org.getBranchOrg())){
                                 if (StringUtils.isNotEmpty(org.getManagerCode())){
                                     candidateUserIdList.add(org.getManagerCode());
                                 }
                             }
                     else  if (UserAssignConstants.USERASSIGN_LEADER_OF_FIRST_ORG.equals(assignSetting.getChooseRule())){ //一级部门
                             if (OrgConstants.LEVEL_FIRST_ORG.equals(org.getBranchOrg())){
                                 String managerCode = org.getManagerCode();
                                 if (!StringUtils.isBlank(managerCode)){
                                     candidateUserIdList.add(managerCode);
                                 }
                             } else  if (OrgConstants.LEVEL_SECOND_ORG.equals(org.getBranchOrg())){
                                 if  (!StringUtils.isBlank(org.getParentCode())) {
                                     OrgEntity orgParent  = orgService.queryOrgByCode(org.getParentCode());
                                    if  (orgParent!= null  && !StringUtils.isBlank(orgParent.getManagerCode())) {
                                        String managerCode = orgParent.getManagerCode();
                                        candidateUserIdList.add(managerCode);
                                      }
                                  }
                             else  if (OrgConstants.LEVEL_THIRD_ORG.equals(org.getBranchOrg())){
                                 if  (!StringUtils.isBlank(org.getParentCode())) {
                                     OrgEntity orgParent  = orgService.queryOrgByCode(org.getParentCode());
                                     if  (orgParent !=  null  && !StringUtils.isBlank(orgParent.getParentCode())) {
                                         OrgEntity orgParentPa  = orgService.queryOrgByCode(orgParent.getParentCode());
                                    if  (orgParentPa!= null  && !StringUtils.isBlank(orgParentPa.getManagerCode())) {
                                        String managerCode = orgParentPa.getManagerCode();
                                        candidateUserIdList.add(managerCode);
                                      }
                                  }
                                 }
                             else  if (OrgConstants.LEVEL_PLATFORM.equals(org.getBranchOrg())){
                                 if (StringUtils.isNotEmpty(org.getManagerCode())){
                                     candidateUserIdList.add(org.getManagerCode());
                                 }
                            
                   
                }
          }
     }

根据汇报关系,目前有发起人本人,发起人直接主管、越级主管、第二越级主管。同样,除发起人外,还可以通过表单数据指定的工号或用户名找出其汇报关系。

基于角色,同基于组织架构的成员规则相同,即将该角色下的所有成员加入审批人列表

当部门和角色均全选时,将按照其交集不空,输出交集;否则输出全集的逻辑进一步筛选。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  if (candidateUserIdList.isEmpty()){
         if  (assignSetting !=  null ){
             //获取直接指定的用户
             if  (StringUtils.isNotEmpty(assignSetting.getUserId())) {
                 String[] userIds = assignSetting.getUserId().split( "[,]" );
                 for  (String user : userIds) { 
                     candidateUserIdList.add(user);
                 }
             }
             
             //通过角色和部门选择用户
             Set<String> roleAndDeptSet = getRoleAndDept(assignSetting);
             if (!CollectionUtils.isEmpty(roleAndDeptSet)){
                  candidateUserIdList.addAll(roleAndDeptSet);
             }
             String startAssignee =  "" ;
             if (StringUtils.isNotEmpty(flowRunTimeInfo.getProcessExecutionId())){
                 startAssignee = processExecutionService.queryById(flowRunTimeInfo.getProcessExecutionId()).getCreateUserCode();
             else  {
                 startAssignee = OAUserContext.getUserCode();
             }
 
             HashSet<String> allUser = chooseCandidateUser(startAssignee, assignSetting, candidateUserIdList);
             if (!CollectionUtils.isEmpty(allUser)){
                 candidateUserIdList = allUser;
             }
         }
     }

如汇报关系与组织架构的基准并非来自发起人或发起人所属组织时,isForm(即来自表单)需勾选且需填写字段名称。该字段名称所代表的用户或组织即作为智能选人规则的基准。目前情况下,如表单字段值有多个,则只代表直接输出字段所指定的用户,如果无法筛选出用户,则审批人列表显示为空。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
   private  String getFiledValueByAssignSetting(UserAssignEntity assignSetting, FlowRunTime flowRunTime) {
         String fieldName_ = assignSetting.getFieldName();
         String[] fieldNames = fieldName_.split( "[,]" );
         String formData = flowRunTime.getFormData();
         String formDefineId = flowRunTime.getFormDefineId();
         String fieldValue =  "" ;
         for (String fieldName : fieldNames){
             List<Map<String, Object>> filedValuelist = formDataService.resolveFormData(formDefineId, formData);
             if (filedValuelist !=  null  && filedValuelist.size() >  0 ){
                 Map<String, String> valueMap =  new  HashMap<String, String>();
                 for (Map<String, Object> map : filedValuelist){
                     for (String key : map.keySet()){
                         if (FormDefineConstants.FORMDEFINE_MAP.equals(key)){
                             valueMap = (Map<String, String>)map.get(key);
                         }
                     }
                 }
                 if (!valueMap.isEmpty()){
                     String value = valueMap.get(fieldName);
                     if (StringUtils.isNotEmpty(value)){
                         if ( "" .equals(fieldValue) ){
                             fieldValue = value;
                         else  {
                             fieldValue = fieldValue +  ","  + value;
                         }  
                     }
                 }
             else  {
                 throw  new  UserAssignException(UserAssignConstants.USERASSIGN_EXCEPTION_FORM_ERROR);
             }
         }
         return  fieldValue;
     }

另外,我们还提供了直接指定审批人的方法,对一些特殊无法通过以上逻辑筛选,但审批人又固定的即可直接指定审批人。

三、对智能选人规则及任务分配的展望

通过以上分析也可以看出,审批人可以在流程运行期间通过某些规则直接指定,所以可以将其称为建模期(或定义期)指派。即通过流程定义进行建模的期间,直接为流程中的活动指派参与者的集合。而在运行期间才能指定审批人(3.1、3.2)或进行分配任务(3.3、3.4、3.5)的则被称为运行期规则。

3.1同前驱

  顾名思义,就指的是在流程的某个环节的办理人,与当前流程当前环节的某个前驱节眯是同一个人,这也与工作流“保持熟悉(Retain familiar)”原则相切合。

同前驱的选人规则目前无法通过建模期指派实现,比如以下场景:

1.招聘需求有“编制管理员审核”(审批候选人可能有多个)以及“招聘管理员签收”环节,其中要求“招聘管理员签收”环节必须与其前驱节点“编制管理员审核”确定;

2.档案借阅的借出环节与还进环节也要求必须是同一个人。

目前我们已经就这种需求形成两种方案:一种是杜提出的在前驱中后置将某代表其同后继的隐藏工号字段填充,后继则使用表单字段选人将审批人选出;一种是我提出的用同前驱节点及execution id遍历历史任务表,将前驱节点历史审批人拿出做为后继节点审批人。

3.2基于历史、基于能力、随机或循环

    以上的三种选人规则,是一种自学习自组织的选人方法。如通过以上选人得出的任选审批人为多个,那么可以使用以上规则筛选为一个或将多人按以规则排序。

  基于历史的是一种笼统的概念,又可以细分为当前任务最少、完成历史任务最多等规则;基于能力则可细分为当前列表中职务最大、层级最高、办理任务耗时最少等;随机则是指列表元素的先后顺序或指定用户是随机的,循环是指列表元素的先后顺序或指定是循环呈现的。

3.3委托\代理、移交\转交

委托是一种运行期间的智能选人模式。对于一个人工任务,在当前活动的参与者出差或休假期间,会用到委托功能。针对委托目前形成的方案是委托模块(委托表除用作工作流选人外,还可能用作),委托表结构如下:

委托人在休假期间新建一个委托,标识委托人、委托时间以及委托的流程(有一些重要流程可能不允许委托)。在每次审批人选出后需要遍历委托表中的有效数据,如当前审批人存在有效委托,则将审批人替换为被委托人,显示名为“被委托人(委托人代理)”,向委托人和被委托人均发送邮件,同时在我的工作中增加“代理工作”模块,将我被代理审批的工作展示出来。

另外的一个委托实现方案来自《流程的永恒之道》一书,即通过另外一个工作流请假单或工作代理单的方式将代理数据写入。代理数据等同于上文中的委托数据。

关于移交\转交:在当前审批者可以使用工作流功能,但由于职责变化或其他原因,将本任务直接转与其他用户。移交\转交功能是下文强行更改审批人的特例。

某用户将属于自己的且正在执行的任务移交给其他人执行。移交模式与委托模式的区别在于,委托是在事前,而移交模式是在事后。

3.4加签

  对于会签节点,存在这样一种可能,会签的目前某参与者或者管理员认为有必要临时将某用户加入会签人列表并为其下发会签任务。加签的实现方案即是:将当前子任务的父任务取出,基于父任务生成一个新的子任务,然后将该子任务分配给该临时用户,并发送提醒邮件。

3.5强行更改审批人

用于某些原因(如当前审批人离职或休假),在运行期间需要将某审批单改分配给另外的用户。逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
  /** 
     
      * @author chao.gao
      * @date 2014-12-26 上午9:43:40
      * @see com.gaochao.oa.module.bpm.workflow.api.server.service.IProcessActivityService#changePeople(com.gaochao.oa.module.bpm.workflow.api.shared.dto.FlowRunTime)
      * @param flowRunTime
      * @return
      */
     @Override
     public  boolean  changePeople(FlowRunTime flowRunTime,TaskVo task) {
         if (StringUtils.isNotEmpty(flowRunTime.getCurrentUser())){
             ProcessExecutionEntity processExecutionEntity = processExecutionService.queryProcessExecutionByPkValue(flowRunTime.getPkValue());
             
             List<Task> list = jbpmOperatorService.getTasksByProcessInstanceId(processExecutionEntity.getProcessInstanceId()); 
             processExecutionService.changePeople(flowRunTime);         
 
             MailSettingEntity mailSetting = mailSettingService.queryMailSetting();              
 
             Map<String, List<MailblackwhitelistEntity>> mailBlackWhiteListMap = mailblackwhitelistService.queryMailBlackWhiteList();
             //设置流程处理ID
             flowRunTime.setProcessExecutionId(processExecutionEntity.getId());
             
             for ( int  i= 0 ;i<list.size();i++){
                 Task task1 = list.get(i);
                 task.setTaskId(task1.getId());
                 processTaskService.changePeople(task);
                 //设置任务Id
                 flowRunTime.setNextTaskId(task1.getId());
                 //经办人
                 final  String assign = flowRunTime.getCurrentUser();
                 jbpmOperatorService.assignTask(task1.getId(), assign);
                         
                 //发送邮件
                 EmailUtil.sendMail2Proxy(assign, flowRunTime, mailSetting, mailBlackWhiteListMap);
             }
         } else {
             throw  new  ProcessActivityException( "没有选择人员" );
         }
         
         return  true ;
     }







     本文转自 gaochaojs 51CTO博客,原文链接:http://blog.51cto.com/jncumter/1614081,如需转载请自行联系原作者



相关文章
预约抢单互助系统开发详细功能/需求方案/步骤功能/逻辑项目/源码案例
The development model of appointment and order grabbing mutual assistance system is a widely used development model on mutual assistance service platforms. It adopts a combination of appointment and order grabbing modes, allowing users to make appointments or actively participate in mutual assistanc
|
7月前
|
安全
dapp互助预约排单系统开发步骤指南/案例设计/规则详细/方案逻辑/源码程序
-Determine the core functions and objectives of the system, understand user needs and expectations.
|
8天前
|
JavaScript Java 测试技术
基于Java的物流配送人员车辆调度管理系统的设计与实现(源码+lw+部署文档+讲解等)
基于Java的物流配送人员车辆调度管理系统的设计与实现(源码+lw+部署文档+讲解等)
23 0
|
6月前
21activiti - 流程管理定义(办理任务)
21activiti - 流程管理定义(办理任务)
18 0
|
7月前
|
存储 安全 前端开发
预约上门按摩项目系统开发详细流程/案例介绍/功能逻辑/需求方案/源码模式
确定系统的目标用户,例如个人用户或企业用户。 - 确定系统的核心功能,如用户注册和登录、服务列表和预约管理、支付和评价反馈等。 - 确定技术平台和开发语言,如Web应用还是移动应用,以及开发语言和框架的选择。 - 制定项目计划,并确定开发阶段和时间安排。
|
7月前
|
安全
上门按摩预约系统开发方案项目/案例详细/需求逻辑/流程设计/源码功能
Implement a user authentication mechanism to ensure the authenticity and security of user identities.
|
10月前
|
测试技术 项目管理
CMMI流程—配置管理流程
CMMI流程—配置管理流程
402 0
|
10月前
智能排班系统 【管理系统功能、操作说明——中篇】
智能排班系统 【管理系统功能、操作说明——中篇】
186 1
|
Java 数据库连接
JBPM学习(一):实现一个简单的工作流例子全过程
本文主要讲实现一个简单的工作流例子全过程
283 0
JBPM学习(一):实现一个简单的工作流例子全过程
|
XML 数据格式
工作流 自定义表单 挂靠流程 模块设计方案
工作流 自定义表单 挂靠流程 模块设计方案
234 0
工作流 自定义表单 挂靠流程 模块设计方案

热门文章

最新文章