尧图精选

Java审批流程的轻量级表结构设计

🕒 发布时间:2026/10/2 18:12:57 📁 来源:尧图网络
1. 项目概述一个真正能跑起来的审批流程表结构设计你有没有遇到过这样的情况业务方拍着桌子说“这个审批流程下周就要上线”开发组长转头就甩给你一张手绘的流程图上面写着“申请人→部门经理→财务→CEO”然后补一句“数据库你看着建别太复杂但得能扩展”。第二天你打开IDEA新建一个Spring Boot项目对着空荡荡的src/main/resources发呆——表怎么建状态字段用int还是String驳回后是回到上一节点还是直接打回申请人历史记录要不要存这些看似基础的问题恰恰是审批系统崩盘的第一道裂缝。我做过7个不同行业的审批系统从制造业的物料领用审批到互联网公司的合同用印流程再到政务系统的公文流转踩过的坑比写过的SQL还多。今天这篇就是把“JAVA审批流程,一个简单表结构设计方案By OU”这个标题背后的真实战场掰开揉碎讲清楚。它不讲BPMN规范不堆Spring Flow源码只聚焦一件事用最少的表、最直白的字段、最易维护的逻辑让审批流程在Java后端稳稳跑起来。核心关键词就是JAVA、审批流程、表结构设计、OU——这里的OU不是指“组织单元”Organization Unit那种抽象概念而是指实际开发中那个必须落地的、带业务含义的“操作单元”比如“采购部”、“华东大区”、“风控中心”它是流程节点的执行主体也是权限控制的最小粒度。适合刚接手审批模块的Java初级/中级开发者也适合需要快速验证流程模型的产品经理。如果你正被“流程引擎太重”“自研又怕漏掉边界”“表设计反复推倒重来”这些问题卡住这篇就是为你写的实操手册。2. 整体设计思路与方案选型解析2.1 为什么放弃“流程引擎”选择“轻量级状态机表驱动”市面上有Activiti、Flowable、Camunda这些成熟的流程引擎它们功能强大支持BPMN可视化建模、并行网关、子流程嵌套。但现实是80%的内部审批需求根本用不到这些。我见过一个报销系统硬上了Flowable结果90%的流程节点都是“提交→审核→通过/驳回”这种线性三步走剩下10%的“多级会签”用脚本硬编码实现最后运维成本高得离谱每次改一个节点要画图、部署、重启服务测试还得配环境。而“轻量级状态机表驱动”的方案核心思想就一句话把流程逻辑从代码里抽出来变成可配置、可追溯、可审计的数据。它不追求“万能”只解决“够用”和“可控”。具体来说就是用一张process_definition表定义流程模板一张process_instance表记录每次运行的实例再用process_task表管理每个环节的任务。所有状态跳转规则都通过process_transition这张关系表来描述“当当前状态是‘待部门经理审核’且操作类型是‘同意’则下一状态为‘待财务审核’”。这样做的好处非常实在第一开发快建表、写Mapper、写Service三天就能跑通一个完整流程第二运维稳改流程不用动代码改几条数据库记录就行第三审计清所有状态变更都有process_log日志表记录谁在什么时候点了什么按钮查表一目了然。当然它也有明确的边界——不适合需要动态插入节点、条件分支极其复杂的场景比如“如果合同金额50万自动触发法务复核否则跳过”。这种需求要么加个简单的if判断要么就该考虑引入真正的引擎了。但对标题里强调的“简单表结构设计方案”这个方案就是黄金分割点。2.2 “OU”在表结构中的真实定位不是组织架构而是执行上下文标题里的“OU”很容易让人联想到LDAP里的组织单元或者Spring Security里的Authority。但在审批流程的语境下OU的含义必须落地到业务层面。它不是静态的部门树而是流程执行时的动态上下文载体。举个例子一个采购申请流程是“申请人→采购部专员→采购部经理→财务部→CEO”。这里“采购部专员”和“采购部经理”同属“采购部”这个OU但他们的审批权限、可操作动作完全不同。所以在我们的设计里OU不单独建一张ou表去存组织架构而是在process_task任务表里用ou_code字段存储执行该任务的OU唯一标识比如proc_purchasing_specialist、proc_purchasing_manager。同时process_definition流程定义表里每个节点node_code会关联一个ou_code表示这个节点由哪个OU来执行。这样做的好处是解耦组织架构可以随时调整比如采购部拆分成“设备采购组”和“耗材采购组”只要把对应节点的ou_code更新一下流程逻辑完全不受影响。更重要的是它天然支持“一人多岗”张三既是“采购部专员”又是“风控中心顾问”他的账号在登录时系统会根据当前流程节点的ou_code动态加载他在这个OU下的角色和权限。这比在用户表里硬塞一堆“role_id”字段要清晰得多。很多初学者喜欢建一张user_ou_relation表把用户和OU多对多关联起来这在初期看起来很“规范”但实际运行中你会发现90%的查询都绕不开“当前流程节点当前操作人”这个组合强行拆分反而增加了JOIN的复杂度和性能损耗。所以我们的方案是OU是流程的属性不是用户的属性一切围绕流程实例展开。2.3 表结构设计的四大核心原则原子性、可追溯、无歧义、易扩展设计任何表结构都不能脱离业务场景空谈范式。针对审批流程我们定下四条铁律第一原子性每张表只负责一个明确的职责。process_definition只管“流程长什么样”process_instance只管“这次流程跑得怎么样”process_task只管“谁在哪个环节干了什么”。绝不允许出现一张表既存流程定义又存实例状态那等于把大象和蚂蚁关在一个笼子里后期维护就是灾难。第二可追溯所有关键操作必须留痕。process_log日志表不是可选项而是必选项。它不仅要记录“谁在什么时候做了什么”还要记录“操作前的状态”和“操作后的状态”以及“触发操作的来源”是前端按钮点击还是定时任务自动触发。有一次客户投诉“流程卡在财务审核没人处理”我们查日志发现是财务人员点了“同意”但系统因为网络超时没收到回调导致状态没更新。没有这条日志问题根本无法定位。第三无歧义字段命名拒绝缩写和模糊词。比如状态字段绝不用status这种泛泛之词而是用current_status并配上完整的枚举注释“SUBMITTED(已提交), DEPARTMENT_REVIEWING(部门审核中), FINANCE_REVIEWING(财务审核中), APPROVED(已批准), REJECTED(已驳回), CANCELLED(已取消)”。再比如“操作类型”不用action而用operation_type值固定为APPROVE,REJECT,TRANSFER,RETURN_TO_APPLICANT。这样新同事看一眼字段名和注释就知道业务含义不需要翻代码猜。第四易扩展预留字段不是摆设。process_instance表里我们加了ext_data_json字段类型是TEXT。它不存业务主数据那些放business_data表里而是存流程特有的、结构不固定的元信息比如“本次审批的紧急程度标记为‘加急’”“驳回时附带的语音留言ID”。这样当业务方突然提出“要给审批加个‘加急’按钮”我们不用改表结构只需在代码里解析这个JSON字段就行。同样process_task表里有个assignee_rule字段存的是一个简单的表达式字符串比如user_role FINANCE_MANAGER dept_code FINANCE未来支持更复杂的分配规则时这个字段就是升级的入口而不是推倒重来。3. 核心表结构详解与字段设计逻辑3.1 流程定义表process_definition流程的“宪法”这张表是整个审批体系的基石它定义了流程的骨架。它的设计目标是让非技术人员也能看懂流程是怎么走的。因此字段设计极度重视可读性和业务映射。CREATE TABLE process_definition ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, code varchar(64) NOT NULL COMMENT 流程编码全局唯一如 procurement_v1, leave_v2, name varchar(128) NOT NULL COMMENT 流程名称如 采购申请流程、员工请假流程, description varchar(512) DEFAULT NULL COMMENT 流程描述用于业务方理解, version int NOT NULL DEFAULT 1 COMMENT 版本号每次修改流程定义需1, is_active tinyint NOT NULL DEFAULT 1 COMMENT 是否启用0-停用1-启用, created_by varchar(64) NOT NULL COMMENT 创建人, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_by varchar(64) DEFAULT NULL COMMENT 最后更新人, updated_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 最后更新时间, PRIMARY KEY (id), UNIQUE KEY uk_code_version (code,version) COMMENT 流程编码版本号唯一 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT流程定义主表;关键字段解析code这是流程的身份证。它必须是业务友好的比如procurement_v1而不是flow_001。版本号version和code组成联合唯一索引确保每次发布新版本旧版本依然可用避免线上流程突然中断。is_active这个字段救过我三次命。有一次新版本流程上线前测试环境发现一个严重BUG我们立刻把生产环境的is_active设为0流量自动切回旧版本5分钟搞定零 downtime。created_time和updated_time这两个时间戳不是为了“好看”而是为了审计。当业务方问“这个流程是什么时候上线的”你直接查表比翻Git记录快十倍。这张表本身不包含节点信息节点信息放在另一张process_node表里这是为了遵循“单一职责”原则。process_node表结构如下CREATE TABLE process_node ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, definition_id bigint NOT NULL COMMENT 关联的流程定义ID, node_code varchar(64) NOT NULL COMMENT 节点编码如 submit, dept_review, finance_review, node_name varchar(128) NOT NULL COMMENT 节点名称如 提交、部门审核、财务审核, ou_code varchar(64) NOT NULL COMMENT 执行此节点的OU编码如 proc_dept_manager, proc_finance, sort_order int NOT NULL DEFAULT 0 COMMENT 节点排序序号决定流程走向, is_start_node tinyint NOT NULL DEFAULT 0 COMMENT 是否为起始节点0-否1-是, is_end_node tinyint NOT NULL DEFAULT 0 COMMENT 是否为结束节点0-否1-是, timeout_hours int DEFAULT 168 COMMENT 超时小时数0表示不超时如部门审核超时7天, PRIMARY KEY (id), KEY idx_def_id (definition_id), CONSTRAINT fk_node_def_id FOREIGN KEY (definition_id) REFERENCES process_definition (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT流程节点定义表;这里ou_code再次出现它和process_definition.code一起构成了流程执行的“坐标系”。sort_order决定了节点的默认顺序但真正的流转逻辑由process_transition表控制这保证了灵活性。timeout_hours字段是经验之谈很多流程卡住不是因为没人处理而是因为没人知道该处理。设置超时系统可以自动发邮件提醒甚至自动升级到上级领导这是提升流程效率的关键杠杆。3.2 流程实例表process_instance每一次审批的“档案袋”如果说process_definition是蓝图那么process_instance就是盖起来的每一栋楼。它记录了某一次具体的审批从开始到结束的全过程。CREATE TABLE process_instance ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, instance_id varchar(128) NOT NULL COMMENT 流程实例ID全局唯一如 PI_20240520_00001, definition_code varchar(64) NOT NULL COMMENT 流程定义编码, definition_version int NOT NULL COMMENT 流程定义版本号, business_key varchar(128) NOT NULL COMMENT 业务单据ID如 PO_20240520_001, business_type varchar(64) NOT NULL COMMENT 业务类型如 PROCUREMENT, LEAVE, CONTRACT, current_status varchar(32) NOT NULL COMMENT 当前状态枚举值SUBMITTED, DEPARTMENT_REVIEWING, ..., current_node_code varchar(64) DEFAULT NULL COMMENT 当前所在节点编码, applicant_id varchar(64) NOT NULL COMMENT 申请人ID, applicant_name varchar(128) NOT NULL COMMENT 申请人姓名, start_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 流程启动时间, end_time datetime DEFAULT NULL COMMENT 流程结束时间NULL表示未结束, completed_by varchar(64) DEFAULT NULL COMMENT 最终完成人ID, ext_data_json text COMMENT 扩展JSON数据存流程特有元信息, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_instance_id (instance_id), KEY idx_bus_key (business_key), KEY idx_def_code_ver (definition_code,definition_version), KEY idx_applicant (applicant_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT流程实例主表;关键字段解析instance_id这是流程的“身份证号”格式为PI_年月日_流水号便于人工识别和排查。它和business_key业务单据ID是两个维度的索引前者用于流程追踪后者用于业务关联。current_status和current_node_code这两个字段是状态机的核心。current_status是宏观状态current_node_code是微观位置。比如当current_status是DEPARTMENT_REVIEWING时current_node_code一定是dept_review。但反过来current_node_code为dept_review时current_status未必是DEPARTMENT_REVIEWING因为可能还在“等待分配”状态。这种分离让状态判断更精准。ext_data_json前面提到的预留字段。实操中我们用它存一个Map比如{urgency:URGENT,reason_for_reject:缺少发票}。Java代码里用Jackson直接序列化/反序列化干净利落。completed_by这个字段常被忽略但它对审计至关重要。当流程走到APPROVED或REJECTED状态时必须记录是谁按下了最终按钮。有一次法务部质疑“谁批准了这份高风险合同”我们直接查completed_by3秒给出答案避免了一场扯皮。3.3 流程任务表process_task审批环节的“工单”process_task表是流程中最活跃的表它代表了每一个待办、已办、已转交的具体任务。CREATE TABLE process_task ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, task_id varchar(128) NOT NULL COMMENT 任务ID全局唯一如 TASK_20240520_00001, instance_id varchar(128) NOT NULL COMMENT 所属流程实例ID, node_code varchar(64) NOT NULL COMMENT 所属节点编码, ou_code varchar(64) NOT NULL COMMENT 执行OU编码, assignee_id varchar(64) DEFAULT NULL COMMENT 当前处理人IDNULL表示未分配, assignee_name varchar(128) DEFAULT NULL COMMENT 当前处理人姓名, status varchar(32) NOT NULL COMMENT 任务状态PENDING(待处理), PROCESSING(处理中), COMPLETED(已完成), CANCELLED(已取消), created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 任务创建时间, claimed_time datetime DEFAULT NULL COMMENT 认领时间, completed_time datetime DEFAULT NULL COMMENT 完成时间, operation_type varchar(32) DEFAULT NULL COMMENT 操作类型APPROVE, REJECT, TRANSFER, RETURN_TO_APPLICANT, remark varchar(512) DEFAULT NULL COMMENT 处理意见, ext_data_json text COMMENT 任务扩展数据, PRIMARY KEY (id), UNIQUE KEY uk_task_id (task_id), KEY idx_instance_id (instance_id), KEY idx_assignee (assignee_id), KEY idx_node_ou (node_code,ou_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT流程任务表;关键字段解析task_id任务的独立ID格式类似TASK_年月日_流水号。它和instance_id是一对多关系一个流程实例可以产生多个任务比如会签时一个节点生成多个任务给不同人。assignee_id和claimed_time这是任务分配的核心。assignee_id为空表示任务已生成但无人认领一旦有人点击“认领”assignee_id被填充claimed_time被记录。这个设计支持“抢单”模式也支持“指派”模式后台直接填assignee_id。status和operation_typestatus是任务的生命周期状态operation_type是用户执行的具体动作。两者结合才能还原完整操作链。比如一个任务status是COMPLETEDoperation_type是REJECT说明它被驳回了如果是APPROVE说明它被通过了。remark这个字段必须存在而且长度要足够。审批意见是法律依据不能只存“同意”两个字。我们要求至少20个字符前端做校验。曾经有个案例销售合同被驳回理由只写了“不行”结果业务方和法务方各执一词最后只能查原始邮件费时费力。现在remark字段强制填写成了最有力的证据。3.4 流程流转规则表process_transition状态跳转的“交通规则”这张表是整个设计的灵魂它用数据定义了“什么情况下流程该怎么走”。CREATE TABLE process_transition ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, definition_code varchar(64) NOT NULL COMMENT 流程定义编码, from_node_code varchar(64) NOT NULL COMMENT 起始节点编码, to_node_code varchar(64) NOT NULL COMMENT 目标节点编码, operation_type varchar(32) NOT NULL COMMENT 触发操作类型APPROVE, REJECT, TRANSFER, condition_expression varchar(512) DEFAULT NULL COMMENT 条件表达式如 amount 100000, is_default tinyint NOT NULL DEFAULT 0 COMMENT 是否为默认路径0-否1-是, sort_order int NOT NULL DEFAULT 0 COMMENT 排序序号用于多条件时的优先级, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_def_from_op (definition_code,from_node_code,operation_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT流程流转规则表;关键字段解析from_node_codeto_node_codeoperation_type这三者构成了一条流转规则的唯一标识。比如from_node_codedept_review,to_node_codefinance_review,operation_typeAPPROVE就定义了“部门经理同意后流程进入财务审核”。condition_expression这是支持“条件分支”的关键。它不是一个复杂的脚本引擎而是一个简单的表达式解析器。我们用SpELSpring Expression Language来解析比如#root.amount 100000。这样当采购金额大于10万时流程走finance_review否则直接走approved。表达式存成字符串运行时动态解析既灵活又安全。is_default当没有匹配到任何条件规则时is_default1的规则就会被执行。这保证了流程永远不会“卡死”。比如所有条件都不满足就默认驳回或者默认打回申请人。这是一个兜底的安全阀。sort_order当一个节点有多个APPROVE规则时比如“金额100万走CEO金额10万走财务其余走部门经理”sort_order决定了它们的匹配优先级。数值越小优先级越高。3.5 流程日志表process_log所有操作的“黑匣子”最后也是最重要的是process_log表。它不参与流程计算但它是所有问题的最终答案。CREATE TABLE process_log ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, log_id varchar(128) NOT NULL COMMENT 日志ID全局唯一, instance_id varchar(128) NOT NULL COMMENT 流程实例ID, task_id varchar(128) DEFAULT NULL COMMENT 任务ID可为空, operator_id varchar(64) NOT NULL COMMENT 操作人ID, operator_name varchar(128) NOT NULL COMMENT 操作人姓名, operation_type varchar(32) NOT NULL COMMENT 操作类型START, ASSIGN, CLAIM, COMPLETE, TRANSFER, CANCEL, before_status varchar(32) DEFAULT NULL COMMENT 操作前状态, after_status varchar(32) DEFAULT NULL COMMENT 操作后状态, remark varchar(512) DEFAULT NULL COMMENT 操作备注, source varchar(32) NOT NULL COMMENT 来源WEB, APP, API, SYSTEM, ip_address varchar(64) DEFAULT NULL COMMENT 操作IP地址, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 操作时间, PRIMARY KEY (id), KEY idx_instance_id (instance_id), KEY idx_operator (operator_id), KEY idx_created_time (created_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT流程操作日志表;关键字段解析log_id日志的唯一ID格式为LOG_年月日_流水号。它和instance_id一起构成了日志查询的黄金组合。before_status和after_status这是日志的精华。它记录了状态变化的“前后对比”。比如一条日志显示before_statusDEPARTMENT_REVIEWING,after_statusFINANCE_REVIEWING,operation_typeCOMPLETE你就知道部门经理完成了审核流程成功推进。source这个字段区分了操作的发起渠道。当发现大量异常操作时你可以先查sourceSYSTEM的日志看看是不是定时任务出了问题再查sourceAPP看看是不是移动端有BUG。ip_address虽然不是所有场景都需要但在金融、政务等强监管领域它是合规的硬性要求。我们用Spring AOP在Controller层统一获取不依赖前端传参杜绝伪造。4. Java后端核心逻辑实现与关键代码片段4.1 状态机引擎的核心TransitionService的实现有了表结构Java代码就是把数据“跑”起来。核心是TransitionService它负责根据当前状态、操作类型和业务数据查询process_transition表找到下一步该去哪里。Service public class TransitionService { Resource private ProcessTransitionMapper processTransitionMapper; Resource private SpelExpressionParser expressionParser; /** * 根据当前节点、操作类型和业务数据计算下一个节点 * param definitionCode 流程定义编码 * param fromNodeCode 当前节点编码 * param operationType 操作类型 * param businessData 业务数据对象用于条件表达式计算 * return 下一个节点编码null表示无匹配规则 */ public String calculateNextNode(String definitionCode, String fromNodeCode, String operationType, Object businessData) { // 1. 查询所有匹配的流转规则 ListProcessTransition transitions processTransitionMapper.selectByCondition( definitionCode, fromNodeCode, operationType); // 2. 如果没有规则返回null由上层处理异常 if (CollectionUtils.isEmpty(transitions)) { return null; } // 3. 按sort_order排序优先匹配高优先级规则 transitions.sort(Comparator.comparingInt(ProcessTransition::getSortOrder)); // 4. 遍历规则计算条件表达式 for (ProcessTransition transition : transitions) { String condition transition.getConditionExpression(); // 如果没有条件或者条件为真则命中 if (StringUtils.isBlank(condition) || evaluateCondition(condition, businessData)) { return transition.getToNodeCode(); } } // 5. 如果所有条件都不满足找默认路径 for (ProcessTransition transition : transitions) { if (transition.getIsDefault() 1) { return transition.getToNodeCode(); } } return null; } /** * 使用SpEL解析并计算条件表达式 * param expression 表达式字符串如 #root.amount 100000 * param rootObject 根对象即业务数据 * return 计算结果 */ private boolean evaluateCondition(String expression, Object rootObject) { try { Expression exp expressionParser.parseExpression(expression); EvaluationContext context new StandardEvaluationContext(); context.setRootObject(rootObject); return exp.getValue(context, Boolean.class); } catch (Exception e) { // 表达式解析失败视为条件不满足避免流程中断 log.warn(SpEL expression evaluation failed: {}, rootObject: {}, expression, rootObject, e); return false; } } }这段代码的关键在于容错性evaluateCondition方法里任何SpEL解析异常都捕获并返回false而不是抛出异常。因为流程不能因为一个表达式写错了就卡死这是生产环境的基本底线。优先级sort_order决定了规则的匹配顺序这是业务方配置流程时的“权重”体现。默认路径最后一步找is_default1的规则是流程不中断的最后保障。这个服务被ProcessInstanceService调用当用户点击“同意”按钮时ProcessInstanceService会先调用它拿到nextNodeCode再创建新的process_task更新process_instance的状态最后写入process_log。整个过程在一个事务里完成保证数据一致性。4.2 任务分配逻辑如何把任务精准地“派”给OU里的正确的人任务分配是审批流程的痛点。常见的错误做法是在process_task表里直接存一个assignee_id然后靠前端或定时任务去“猜”谁该处理。这会导致任务堆积、责任不清。我们的方案是分配逻辑下沉到Service层由OU的规则驱动。Service public class TaskAssignmentService { Resource private ProcessNodeMapper processNodeMapper; Resource private UserOuRelationMapper userOuRelationMapper; /** * 为指定OU分配任务 * param ouCode OU编码 * param nodeCode 节点编码 * param instanceId 流程实例ID * return 分配到的用户ID列表 */ public ListString assignTaskToOu(String ouCode, String nodeCode, String instanceId) { // 1. 查询该OU下拥有此节点权限的所有用户 // 这里假设权限表是 user_ou_role存用户在OU下的角色 ListUserOuRole roles userOuRelationMapper.selectUsersByOuAndNode(ouCode, nodeCode); // 2. 如果是会签全部分配如果是串签取第一个或按规则轮询 if (SIGN.equals(getAssignmentMode(nodeCode))) { // 会签模式所有人收到任务 return roles.stream().map(UserOuRole::getUserId).collect(Collectors.toList()); } else { // 串签模式取第一个可用用户可扩展为负载均衡算法 if (!roles.isEmpty()) { return Collections.singletonList(roles.get(0).getUserId()); } } // 3. 如果没找到人记录告警但不中断流程 log.warn(No user found for OU:{} and Node:{} in Instance:{}, ouCode, nodeCode, instanceId); return Collections.emptyList(); } /** * 获取节点的分配模式 * param nodeCode 节点编码 * return SIGN(会签) or SEQUENCE(串签) */ private String getAssignmentMode(String nodeCode) { // 从process_node表的某个扩展字段读取或硬编码 // 这里简化为查表 ProcessNode node processNodeMapper.selectByNodeCode(nodeCode); return StringUtils.defaultString(node.getAssignmentMode(), SEQUENCE); } }这个服务的设计哲学是分配不是随机的而是基于OU的权责体系。userOuRelationMapper.selectUsersByOuAndNode()这个方法查询的是“在采购部这个OU里哪些人拥有‘部门经理’这个角色”而不是“张三在不在采购部”。这样当张三调岗到风控中心他的账号在采购部的权限会自动失效任务就不会再派给他。这才是真正的权限管控。4.3 审批操作的原子性保障Service层的事务与幂等设计审批操作尤其是“同意”和“驳回”必须是原子的。一次点击要么全部成功要么全部失败绝不能出现“状态更新了但日志没写”的情况。同时还要防重复提交。Service Transactional(rollbackFor Exception.class) public class ProcessInstanceService { Resource private ProcessInstanceMapper processInstanceMapper; Resource private ProcessTaskMapper processTaskMapper; Resource private ProcessLogMapper processLogMapper; Resource private TransitionService transitionService; /** * 处理审批操作同意/驳回 * param taskId 任务ID * param operatorId 操作人ID * param operationType 操作类型 * param remark 意见 * param businessData 业务数据 */ public void handleApproval(String taskId, String operatorId, String operationType, String remark, Object businessData) { // 1. 根据taskId查询当前任务 ProcessTask task processTaskMapper.selectById(taskId); if (task null || !PENDING.equals(task.getStatus())) { throw new BusinessException(任务不存在或不可操作); } // 2. 查询对应的流程实例 ProcessInstance instance processInstanceMapper.selectByInstanceId(task.getInstanceId()); if (instance null) { throw new BusinessException(流程实例不存在); } // 3. 计算下一个节点 String nextNodeCode transitionService.calculateNextNode( instance.getDefinitionCode(), task.getNodeCode(), operationType, businessData); if (nextNodeCode null) { throw new BusinessException(未找到匹配的流转规则); } // 4. 更新当前任务状态 task.setStatus(COMPLETED); task.setOperationType(operationType); task.setRemark(remark); task.setCompletedTime(new Date()); processTaskMapper.updateById(task); // 5. 更新流程实例状态 instance.setCurrentStatus(getStatusByNode(nextNodeCode)); instance.setCurrentNodeCode(nextNodeCode); // 如果是结束节点设置end_time if (isEndNode(nextNodeCode)) { instance.setEndTime(new Date()); instance.setCompletedBy(operatorId); } processInstanceMapper.updateById(instance); // 6. 创建新任务如果nextNodeCode不是结束节点 if (!isEndNode(nextNodeCode)) { createNewTask(instance, nextNodeCode, operatorId); } // 7. 写入操作日志 writeProcessLog(instance, task, operatorId, operationType, remark); } /** * 幂等性校验检查该操作是否已执行 * param taskId 任务ID * param operatorId 操作人ID * param operationType 操作类型 * return true表示已存在无需重复执行 */ private boolean isOperationDuplicate(String taskId, String operatorId, String operationType) { // 查询日志表看是否有相同task_id、operator_id、operation_type的记录 return processLogMapper.existsByTaskAndOperator(taskId, operatorId, operationType) 0; } }关键点Transactional整个方法在一个数据库事务里任何一步失败全部回滚。幂等校验isOperationDuplicate方法在业务逻辑最开始就执行防止用户手抖连点两次造成重复审批。这个校验基于process_log表而不是内存或Redis因为日志表是最终一致性的权威来源。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →