
工作流任务的轮询自动分配——一个存储过程实现的公平派单引擎政府审批系统的任务分配有三个原则公平轮询、一人不进多环节、本级无人向上分配。这套逻辑没有放在Java代码里而是写成了一个Oracle存储过程——prc_alloc。本文逐段拆解这个111行的派单引擎。文章目录工作流任务的轮询自动分配——一个存储过程实现的公平派单引擎一、背景为什么需要一个自动派单存储过程二、入参——角色和部门的编码三、候选人列表——按角色部门筛选排除请假人员四、轮询核心——找到下一个该轮的人五、一人不进多环节——跳过已参与过的人六、执行分配——更新任务写日志七、收尾——保存轮询位置八、完整分配流程总结九、为什么可以写一篇博客一、背景为什么需要一个自动派单存储过程政务审批系统里每个环节的任务需要分配给具体的经办人。传统做法是任务到了某个环节→部门主管手动指定谁处理。但实际运行中手动分配有两个问题不公平——主管记不住上次分给了谁容易集中在某几个人身上一个人审多个环节——申请和审批是同一个人失去了审核的意义解决这两个问题需要一个自动派单引擎任务到了角色部门系统自动按轮询顺序找到下一个应该处理的人同时跳过已经在这个流程中出现过的人。二、入参——角色和部门的编码createorreplaceprocedureprc_alloc(prm_assignee_invarchar2)is入参prm_assignee_的格式是#role_id#dept_id——角色编号用#号包裹在前面后面跟着部门编号。v_role_id :substr(prm_assignee_,2,instr(prm_assignee_,#)-2);v_aab301 :substr(prm_assignee_,instr(prm_assignee_,#)1);解析后拿到两个关键信息角色ID如社保审核岗和部门编号如信息科。这就是派单的范围——从这个角色部门的所有人中轮询分配。三、候选人列表——按角色部门筛选排除请假人员selecta.role_id,b.psn_id,a.baz001bulkcollectintov_rowsfromrole_person_map a,op_person bwherea.psn_idb.psn_idanda.role_idv_role_idandb.dept_idv_aab301andnotexists(select1fromop_person_st cwherec.psn_idb.psn_idandsysdatec.begin_and(c.end_sysdateorc.end_isnull)andstatus!0)orderby2;BULK COLLECT INTO一次性把候选人列表加载到PL/SQL数组v_rows里之后不碰数据库。三个筛选条件条件作用role_id v_role_id有这个角色的人dept_id v_aab301属于这个部门的人not exists (op_person_st ...)剔除当前正在请假/离岗的人员如果v_rows.count 0——说明本级部门没有人具备这个角色。这种情况下外层的调用方会检测到空结果用上级部门的角色ID重新调一次prc_alloc。这就是本级无人向上分配。四、轮询核心——找到下一个该轮的人-- 查询上次分配到的人selectpsn_idintov_curr_psnfromt_curr_psnwhererole_idv_role_idandaab301v_aab301;-- 候选人列表中定位上次的人从下一位开始foriin1..v_rows.countloopifv_curr_psnv_rows(i).psn_idthenn_fast :i;-- 找到上次停止的位置n_fast :n_fast1;-- 移到下一位ifn_fastv_rows.countthenn_fast :1;-- 到底了绕回头endif;EXIT;endif;endloop;t_curr_psn是轮询的指针表——每个角色部门存一条记录记着上次任务分配给了谁。下次分配时从指针位置的下一个人开始。如果上次是最后一个绕回第一个人。这就是Round-Robin的经典实现。五、一人不进多环节——跳过已参与过的人foriinn_fast..v_rows.countloop-- 检查这个人在同一个流程实例中是否已出现过selectcount(*)inton_countfromAct_Hi_Taskinstwhereproc_inst_id_v_proc_inst_id_andassignee_v_rows(i).psn_idandtask_def_key_!v_task_def_key_;-- 已出现在其他环节 → 跳过ifn_count0thenv_move :0;continue;endif;v_move :1;-- 分配任务...endloop;这个检查保证了审批的人不能和申请的人是同一个人——虽然Act_Hi_Taskinst是按流程实例和经办人查的而且加了task_def_key_ ! v_task_def_key_意味着只要此人在这个流程的其他环节出现过无论是什么角色本轮就跳过。流程从头到尾同一个人最多只能参与一个环节。这是政务审批的硬约束——不能自己申请自己批。六、执行分配——更新任务写日志-- 记录旧分配审计追溯insertintoold_task(task_id,old_assignee_,new_assignee_)values(v_task_id,prm_assignee_,v_rows(i).psn_id);-- 更新运行时任务表updateact_ru_tasksetassignee_v_rows(i).psn_idwhereid_v_task_id;-- 同步更新历史任务表updateact_hi_taskinstsetassignee_v_rows(i).psn_idwhereid_v_task_id;-- 保存本次轮询位置下次从这里继续v_curr_psn :v_rows(i).psn_id;三件事old_task写入旧分配记录——谁→谁可追溯。审计时能看到每一笔任务被重新分配了几次同时更新运行时表和历史表——Activiti 的act_ru_task和act_hi_taskinst都需要更新否则历史查询会是旧处理人更新轮询指针——t_curr_psn记录当前处理到的人下次分配从这个人的下一位开始七、收尾——保存轮询位置-- 删除旧的指针记录deletefromt_curr_psnwhererole_idv_role_idandaab301v_aab301;-- 插入新的指针记录insertintot_curr_psn(role_id,psn_id,aab301)values(v_role_id,v_curr_psn,v_aab301);每次分配完一轮任务后更新指针表。先删后插——保持每个角色部门只有一条指针记录。下一批任务过来从这个指针的下一位置继续分。八、完整分配流程总结prc_alloc(#社保审核岗#信息科) │ ├── ① 解析角色ID 部门编号 ├── ② 查询该角色部门的候选人列表排除请假人员 ├── ③ 从 t_curr_psn 读取上次轮询位置 ├── ④ 取所有待分配任务assignee_ prm_assignee_ ├── ⑤ 从上次位置1开始轮询分配 │ ├── 查 Act_Hi_Taskinst → 此人是否已参与该流程 → 是则跳过 │ ├── 分配任务 → 更新 act_ru_task act_hi_taskinst │ └── 写入 old_task 分配记录 ├── ⑥ 保存当前轮询位置到 t_curr_psn └── 完成九、为什么可以写一篇博客一般讲工作流分配的文章用的是Java代码——自定义分配器、候选组、监听器。这套逻辑放在Oracle存储过程里比Java方案的优势性能——候选人的BULK COLLECT 任务游标循环全部在数据库内部完成不用在Java和DB之间反复传数据原子性——一次存储过程调用完成所有分配中间无Java代码介入部署简单——改规则直接改存储过程不用重启应用但缺点也很明显调试麻烦注释掉的dbms_output.put_line就是证据、代码可读性差PL/SQL的游标嵌套、不好做单元测试。这套存储过程的本质是用数据库的能力解决了一个业务调度问题——不是教科书上的最优解但在当时的环境下它是最务实的选择。