简介:这是一份面向Java Web开发者与企业信息化实施人员的OA办公自动化系统项目源码包,围绕需求分析、系统设计、流程引擎、权限管理、移动办公、报表分析与项目实施维护等环节,提供可参考的工程实现与模块组织思路。rar压缩包共收录1212个文件,约17.92MB,以JavaScript、GIF、Java类文件、XML配置、JAR依赖、HTML/CSS/JSP页面及FreeMarker模板等为主,兼有工作流相关字节码、配置和说明文档,能够较完整地呈现传统OA项目的分层结构与前端资源组织。目前已有387人学习,适合需要了解审批流、权限控制、系统集成和后台管理页面搭建的读者。通过梳理其中的Java源码、JSP页面、流程与权限模块以及静态资源,可辅助理解OA系统从业务建模到部署实施的各个环节,为课程设计、毕业项目或企业办公系统二次开发提供结构参照与实现线索。
1. 从一张请假单说起:这套 OA 系统项目的功能边界
上周一个做行政的朋友把一份 OA 系统项目丢给我,说他们三十多号人的请假、报销、用章、公文流转还压在 Excel 加微信群里,想找个能直接跑起来的底座。我拆了两天,这套 OA 系统项目不是只做了登录页的演示壳子:组织架构、角色权限、审批流转、待办消息、附件归档这几条主链路都是通的,代码分层也比较规整。
它适合两类人。一类是想给中小团队快速搭一套内部办公系统的开发者,改改表单和流程节点就能上线;另一类是想拿一个完整的前后端分离工程练手的同学,登录鉴权、分页查询、文件上传、状态机这些常见套路在里面都能找到对应实现。下面按我实际拆解的顺序走:先看技术栈和目录,再进核心链路读代码,然后从建库一路跑到登录,接着是几个我踩过的坑,最后讲怎么把它改成自己的业务系统。
2. 技术栈与目录结构:先认清骨架再动手
拿到一个陌生工程,我一般不会急着git clone完就run,而是先把pom.xml和目录树过一遍,搞清楚它是不是前后端分离、权限模型怎么设计的、有没有引入第三方工作流引擎。这套 OA 系统的骨架不算复杂,属于典型的单体后端加独立前端,读起来负担小。
2.1 技术选型:为什么是单体加自研审批流
后端是 Spring Boot + MyBatis-Plus 的组合,MySQL 存业务数据,Redis 承担会话缓存和待办计数,鉴权走 JWT 无状态令牌,文件默认落本地磁盘,配置里留了对象存储的开关。前端是 Vue 3 + Vite + Element Plus,路由按菜单动态生成,按钮级权限用自定义指令控制显隐。
这里有个值得说的取舍:审批流没有直接引入 Flowable 这类重型引擎,而是自己建了流程定义、流程实例、任务、流转记录四张表,用状态机驱动。常见做法是,流程节点不多、审批规则以串行和简单分支为主的内部系统,自研表驱动比上 BPMN 引擎更好维护,出问题的时候你能直接查表定位,不用去啃引擎的几十张内部表。代价是复杂会签、驳回重走、加签这些高级玩法要自己补,后面二次开发那一章会讲怎么补。
技术栈大致是这样:
| 层次 | 选型 | 在项目里的角色 |
|---|---|---|
| 后端框架 | Spring Boot | 启动、依赖注入、统一异常处理 |
| 持久层 | MyBatis-Plus | 单表 CRUD、分页插件、逻辑删除 |
| 数据库 | MySQL | 用户、部门、流程、表单数据 |
| 缓存 | Redis | 登录会话、待办数量、字典缓存 |
| 鉴权 | JWT + 拦截器 | 接口级鉴权、按钮权限码下发 |
| 前端 | Vue 3 + Vite + Element Plus | 动态路由、表单渲染、列表页 |
| 文件 | 本地磁盘或对象存储 | 附件上传、预览、归档 |
选型上没有特别花哨的东西,都是中小团队能接得住的技术。真正决定这套代码好不好改的,是分层是否清晰,这就得看目录了。
2.2 目录结构:从启动类到 Mapper 的完整分层
工程根目录下分后端和前端两块,后端按业务域拆模块,前端按功能页拆目录。我整理了一份关键路径清单,第一次读源码的时候对照着看会快很多:
| 路径 | 作用 | 二次开发时的改动频率 |
|---|---|---|
oa-admin | 启动类、全局配置、拦截器注册 | 低 |
oa-common | 统一返回体、业务异常、工具类、常量 | 中 |
oa-system | 用户、部门、角色、菜单、字典 | 高 |
oa-flow | 流程定义、实例、任务、审批记录 | 高 |
oa-file | 附件上传、下载、预览 | 中 |
oa-web/src/api | 按模块拆的接口封装 | 高 |
oa-web/src/views | 页面组件,按业务目录组织 | 高 |
oa-web/src/router | 静态路由与动态菜单注入 | 中 |
后端每个业务模块内部都是controller / service / mapper / entity / dto五层,dto负责接收入参和出参转换,entity和数据库表一一对应。这种分层很多人觉得啰嗦,但好处是改接口的时候不会牵一发动全身,前端传参变化只动 DTO,表结构变化只动 entity 和 mapper。我一般会先看oa-common里的统一返回体和异常定义,因为所有 Controller 的返回都套这一层,看懂了后面读接口就顺了。
前端的router目录里有一份静态路由表,登录后拿到的菜单数据会被拼进去,这一点在排查「菜单不显示」类问题的时候特别关键,后面避坑章节会展开。
2.3 配置与环境:数据源、Redis、文件路径怎么改
配置文件在oa-admin/src/main/resources下,按环境拆了application.yml、application-dev.yml、application-prod.yml。第一次跑之前至少要改三处:数据库连接、Redis 地址、附件存储路径。核心片段大概长这样:
server: port: 8080 servlet: context-path: /api # 前端代理要跟这里保持一致 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/oa_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: 127.0.0.1 port: 6379 database: 0 servlet: multipart: max-file-size: 20MB # 单文件上限,跟 Nginx 配置要一起改 max-request-size: 50MB oa: jwt: secret: change-this-to-your-own-secret # 生产必须换掉,否则令牌可伪造 expire-hours: 2 file: storage: local # local 或 oss,切换实现类即可 local-path: D:/oa-upload # Linux 下换成 /data/oa-upload 并确认写权限context-path设成/api之后,前端开发服务器的代理必须指向同一个前缀,否则会出现「接口 404 但浏览器看着像跨域」的玄学现象。jwt.secret是签名密钥,源码里给的默认值只能用于本地调试,上线前务必替换,长度建议不少于 32 位。oa.file.storage是一个策略开关,local走本地磁盘实现,oss走对象存储实现,两个实现类都实现了同一个上传接口,切换时不用改业务代码。
还有一处容易忽略的是 MyBatis-Plus 的逻辑删除配置,deleted字段默认 0 表示未删,查询时会自动拼上deleted = 0。如果你手动写 XML 的关联查询,记得自己补这个条件,不然会查出已删除的数据,这种问题在列表页上表现得很隐蔽。
3. 核心链路拆解:一次审批从提交到归档经过哪些代码
骨架看完,接下来是最值得读的部分。我把这套 OA 系统的主链路拆成四段:登录拿令牌、提交审批生成任务、审批通过推进节点、附件跟着单据走。每一段都有对应的核心类,读懂了这四段,剩下的模块基本都是同一套模板。
3.1 登录鉴权:令牌签发与拦截器校验
登录入口在AuthController,真正的逻辑在AuthService.login()。这段代码值得逐行看,因为它串起了用户查询、密码校验、状态判断、权限加载、令牌签发、会话缓存六个动作:
@Service public class AuthService { @Resource private SysUserMapper userMapper; @Resource private MenuMapper menuMapper; @Resource private JwtUtil jwtUtil; @Resource private PasswordEncoder passwordEncoder; @Resource private StringRedisTemplate redisTemplate; public LoginVO login(LoginDTO dto) { // 1. 按用户名查未删除用户 SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>() .eq(SysUser::getUsername, dto.getUsername()) .eq(SysUser::getDeleted, 0)); // 账号不存在和密码错误返回同一句提示,避免被枚举账号 if (user == null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) { throw new BizException("账号或密码错误"); } // 2. 停用账号直接拦掉 if (user.getStatus() == 0) { throw new BizException("账号已停用,请联系管理员"); } // 3. 加载权限码,前端按钮显隐和接口鉴权共用这一份 List<String> perms = menuMapper.selectPermsByUserId(user.getId()); // 4. 签发令牌,payload 里带用户 id 和权限版本号 String token = jwtUtil.create(user.getId(), user.getPermVersion()); // 5. 写入 Redis,支持续期和服务端踢人 redisTemplate.opsForValue().set("oa:token:" + user.getId(), token, jwtUtil.getExpireHours(), TimeUnit.HOURS); LoginVO vo = new LoginVO(); vo.setToken(token); vo.setUserId(user.getId()); vo.setNickname(user.getNickname()); vo.setPerms(perms); return vo; } }几个参数值得单独说。permVersion是权限版本号,管理员改了角色权限之后这个值加一,拦截器比对令牌里的版本号和数据库里的版本号,不一致就强制重新登录,这是解决「权限改了不生效」最省事的办法。oa:token:{userId}这个 Redis key 让服务端具备踢人能力:校验时除了验签,还要比对缓存里的令牌是否一致,管理员点「强制下线」只要删掉这个 key。passwordEncoder用的是 BCrypt,matches内部做了盐值比对,不要自己写 MD5,源码里也没这么干。
拦截器那边挂在WebMvcConfig上,放行登录、验证码、静态资源三类路径,其余请求统一走JwtInterceptor。拦截器里做三件事:解析令牌、比对 Redis 会话、把用户上下文塞进ThreadLocal,请求结束时在afterCompletion里清理,不然线程池复用会串数据。
3.2 流程流转:四张表撑起一条审批链
自研审批流的核心是四张表,建表语句能直接反映出设计思路:
-- 流程定义:一个业务类型对应一条定义,version 支持灰度 CREATE TABLE oa_flow_def ( id BIGINT PRIMARY KEY AUTO_INCREMENT, flow_key VARCHAR(64) NOT NULL COMMENT '流程标识,如 leave_apply', flow_name VARCHAR(128) NOT NULL COMMENT '流程名称', node_config TEXT NOT NULL COMMENT '节点配置 JSON,按顺序存审批节点', version INT NOT NULL DEFAULT 1, status TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用', UNIQUE KEY uk_key_version (flow_key, version) ) COMMENT '流程定义表'; -- 流程实例:一次提交就是一条实例 CREATE TABLE oa_flow_instance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, flow_key VARCHAR(64) NOT NULL, biz_id BIGINT NOT NULL COMMENT '业务单据 id,如请假单 id', title VARCHAR(255) NOT NULL, submitter_id BIGINT NOT NULL, current_node INT NOT NULL DEFAULT 0 COMMENT '当前节点下标', status TINYINT NOT NULL DEFAULT 0 COMMENT '0审批中 1通过 2驳回 3撤回', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_submitter (submitter_id), KEY idx_biz (flow_key, biz_id) ) COMMENT '流程实例表'; -- 审批任务:待办列表就是查这张表 CREATE TABLE oa_flow_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, instance_id BIGINT NOT NULL, node_index INT NOT NULL, node_name VARCHAR(128) NOT NULL, handler_id BIGINT NOT NULL COMMENT '当前处理人', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待办 1已办', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_handler_status (handler_id, status) ) COMMENT '审批任务表'; -- 流转记录:审计和幂等都靠它 CREATE TABLE oa_flow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, instance_id BIGINT NOT NULL, node_index INT NOT NULL, handler_id BIGINT NOT NULL, action VARCHAR(16) NOT NULL COMMENT 'approve/reject/cancel', comment VARCHAR(500) DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_instance_node_handler (instance_id, node_index, handler_id) ) COMMENT '审批记录表';node_config存的是 JSON 数组,每一项包含节点名、处理人类型(指定人、角色、部门负责人)和处理人 id,服务层解析后按current_node下标顺序推进。uk_instance_node_handler这个唯一索引是关键,它同时承担幂等职责:同一个节点同一个人重复提交审批,第二次会直接撞唯一键,事务里捕获后返回「请勿重复操作」,比在代码里查一遍再插入更可靠。
推进节点的核心方法是FlowService.approve(),逻辑不复杂但顺序不能乱:先查任务并校验处理人身份,再写流转记录,然后判断当前节点是不是最后一个,是就更新实例状态为通过并回调业务侧的完成方法,不是就把current_node加一并为下一节点生成新任务。整个过程包在一个事务里,oa_flow_record的插入放在最前面,一旦重复提交会在这一步抛异常回滚,后面的状态推进不会发生。
驳回的处理稍有不同:实例状态置为驳回,当前待办任务全部关闭,单据回到提交人手上,提交人修改后重新提交会生成一条新的实例,而不是在原实例上回退。这种「驳回即重开」的做法会多几条实例数据,但省掉了回退路径上的状态清理,对中小系统来说更稳。
3.3 待办与消息:计数缓存和状态一致性
待办列表直接查oa_flow_task里handler_id = 当前用户 and status = 0的记录,分页和条件筛选都在 Service 层拼。首页那个「待办数量」小红点,用的是 Redis 里的计数器,key 形如oa:todo:count:{userId},任务生成时incr,审批完成时decr。
这里有个常见的一致性问题:如果计数和数据库写入不在同一个事务边界内,Redis 写失败就会导致数字和列表对不上。这套代码的处理方式是在任务生成后发送一个应用内事件,由监听器异步更新计数,同时提供一个「刷新计数」的兜底接口,前端在进入首页时如果发现数字异常可以手动触发。我一般会在项目里再加一层定时对账,每天凌晨按用户维度重新统计一次,把偏差抹平,毕竟缓存这类东西你永远不能假设它和数据库完全同步。
消息通知部分没有接第三方,走的是站内信表加轮询。每次任务生成往oa_message插一条记录,前端每 30 秒拉一次未读数。这种做法在几十人的团队完全够用,量大了再换 WebSocket 或者消息队列,代码里已经把发送逻辑抽成MessageSender接口,替换实现不影响业务。
3.4 附件归档:上传白名单与预览链路
附件模块的入口是FileController.upload(),接收MultipartFile和一个业务类型参数,落盘路径按「业务类型/年月/雪花 id」组织,避免单目录文件过多。上传前做三件事:校验大小、校验扩展名白名单、校验文件头。扩展名白名单建议只放开pdf/doc/docx/xls/xlsx/png/jpg这几类,文件头校验是为了防止改了后缀的可执行文件混进来,虽然内部系统风险低,但这一步不费事。
预览走的是后端流式输出,Content-Disposition用inline让浏览器直接打开 PDF 和图片,Office 文档则返回下载。数据库里oa_file表记录原始文件名、存储路径、大小、上传人和业务关联 id,业务单据删除时附件不物理删除,只标记逻辑删除,这是留后悔药的做法,误删了还能找回。
归档动作挂在流程实例变为「通过」的回调里,把附件从临时目录挪到归档目录并更新路径。这里要注意路径更新和文件移动的顺序,我一般先移动文件再更新数据库,移动失败就直接抛异常回滚,反过来会出现数据库指向一个不存在的文件。
4. 跑起来:从建库到第一个账号登录
代码读得差不多了,接下来是把它跑起来。这一章按我实际操作的顺序走,建库、起后端、起前端、登录验证,每一步都给命令和验证方式。
4.1 数据库初始化与种子数据
先建库再导脚本,脚本在oa-admin/src/main/resources/sql下,一般分schema.sql和data.sql两个。执行方式有两种,命令行或者客户端工具都行:
# 登录 MySQL 并建库,字符集必须 utf8mb4,否则审批意见里的特殊字符会乱码 mysql -uroot -p -e "CREATE DATABASE oa_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" # 依次导入表结构和种子数据 mysql -uroot -p oa_system < oa-admin/src/main/resources/sql/schema.sql mysql -uroot -p oa_system < oa-admin/src/main/resources/sql/data.sql # 验证:应该能看到用户、菜单、流程定义等表都有数据 mysql -uroot -p oa_system -e "SELECT COUNT(*) FROM sys_user; SELECT flow_key, flow_name FROM oa_flow_def;"schema.sql里包含全部建表语句和索引,data.sql里是初始管理员账号、基础菜单、字典数据和两条示例流程定义。初始密码一般是加密后的固定值,脚本注释里会写明文,第一次登录后立刻改掉。如果导入报外键约束错误,多半是表的创建顺序问题,按脚本里的顺序执行就行,别自己拆开单条跑。
4.2 后端启动与接口自测
后端用 Maven 起,第一次会拉依赖,耐心等:
# 在项目根目录执行,-DskipTests 跳过测试,第一次跑建议先跳 mvn clean package -DskipTests # 开发模式直接跑启动类所在的模块 mvn -pl oa-admin spring-boot:run -Dspring-boot.run.profiles=dev # 或者用打好的 jar 启动,生产环境这么干 java -jar oa-admin/target/oa-admin.jar --spring.profiles.active=dev启动日志里重点看三行:Tomcat 监听的端口、数据源初始化是否成功、Redis 连接是否正常。看到Started OaApplication基本就成了。然后拿登录接口自测一下,确认后端是通的:
# 请求登录接口,注意 context-path 是 /api curl -X POST http://127.0.0.1:8080/api/auth/login \\ -H "Content-Type: application/json" \\ -d '{"username":"admin","password":"脚本注释里的初始密码"}' # 返回体里应该有 token 和 perms 两个字段,token 拿去请求带鉴权的接口 curl http://127.0.0.1:8080/api/system/user/page?pageNum=1&pageSize=10 \\ -H "Authorization: Bearer 上一步拿到的token"如果登录返回 401 而账号密码确认没错,先去日志里找异常栈,大概率是 Redis 没连上导致会话写入失败,或者jwt.secret在某处被覆盖成了空值。
4.3 前端启动与联调代理
前端在oa-web目录,装依赖起服务:
cd oa-web npm install # 依赖装完后确认 node_modules 里没有报错 npm run dev # 默认起在 5173 端口开发服务器的代理配置在vite.config.js里,必须和后端的context-path对上,否则接口全 404:
// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { // 前端请求 /api/xxx 会被转发到后端,路径前缀保持一致 '/api': { target: 'http://127.0.0.1:8080', changeOrigin: true // 注意:后端 context-path 已经是 /api,这里不要再 rewrite 掉前缀 } } } })changeOrigin: true是为了让后端拿到的 Host 头是目标地址,某些校验来源的接口会用到。这里最常见的翻车点是画蛇添足加了一句rewrite: path => path.replace(/^\\/api/, ''),结果后端收到的是/auth/login而不是/api/auth/login,直接 404。记住一个原则:后端 context-path 里有的前缀,代理层原样透传。
浏览器打开http://127.0.0.1:5173,用种子数据里的管理员账号登录,能进首页、侧边栏菜单正常渲染、待办列表有示例数据,这套 OA 系统就算跑通了。
5. 常见问题排查:这套 OA 项目最容易翻车的五个地方
跑通只是开始,改起来才是真正的战场。下面这五条是我在拆这套代码和帮人排查时反复遇到的,每条按现象、原因、解决三步走,遇到同类问题可以直接对号入座。
5.1 登录与鉴权类问题
现象一:登录接口返回成功,页面闪一下又跳回登录页。打开浏览器网络面板能看到首页请求带着令牌,但接口返回 401。原因通常有两个:一是前端存令牌用的 key 和请求拦截器取令牌用的 key 不一致,比如登录页存的是token,请求拦截器读的是Access-Token;二是后端拦截器没有放行OPTIONS预检请求,跨域场景下预检被拦,真正的请求根本没发出去。解决办法是统一在src/utils/request.js里定义常量供两处引用,同时在拦截器的放行列表里加上OPTIONS方法判断,预检请求直接放行不校验令牌。
现象二:管理员改了角色权限,用户刷新页面后按钮还是没变。原因是权限码在登录时一次性下发到前端并存在了本地状态里,后端改了角色菜单关系,前端手里的旧数据不会自动更新。这套代码里预留了permVersion机制,解决方式是管理员保存角色权限时把该角色下所有用户的perm_version加一,用户下次请求时拦截器发现版本不一致,返回特定的状态码,前端捕获后清空本地令牌跳登录页重新走一遍。如果没有这个机制,最土的办法是让用户退出重登,但体验很差。
5.2 流程与数据类问题
现象三:单据提交成功,但审批人的待办列表里什么都没有。按顺序查三处。先看oa_flow_def里对应flow_key的记录status是不是 1,停用的流程定义提交时会走「无可用流程」的分支,但有些实现里这个分支只打日志不抛异常,表现就是静默成功。再看node_config解析出来的第一个节点处理人列表是不是空的,指定人离职或者角色下没有成员都会导致生成不出任务。最后看oa_flow_task表里有没有数据,有数据但列表查不到,那就是待办查询的条件写错了,比如漏了status = 0或者按部门过滤时把处理人过滤掉了。
现象四:审批人连点两次通过,产生了重复的流转记录和重复的后续任务。这是典型的并发提交问题。数据库层面oa_flow_record有唯一索引兜底,但前提是插入语句在事务最前面执行,如果代码先更新任务状态再插记录,两次请求都能更新成功,唯一索引就白设了。解决方式是把「写流转记录」作为第一道闸门,插入冲突直接抛异常回滚,同时前端按钮点击后立刻置灰,双保险。
5.3 环境与文件类问题
现象五:上传附件报 413,或者上传成功但文件名变成乱码。413 是请求体超限,要改三处:Spring 的max-file-size和max-request-size、Nginx 的client_max_body_size、以及网关层如果有的话也有自己的限制,少改一处都不行。文件名乱码多半是编码问题,检查 MySQL 库和表的字符集是不是utf8mb4,检查 Tomcat 的URIEncoding,还要看前端FormData里文件名有没有被二次编码。我一般的做法是后端接收时统一用new String(file.getOriginalFilename().getBytes("ISO-8859-1"), "UTF-8")做一次兜底转换,虽然不优雅,但能挡住大部分历史遗留环境的问题。
另外提一个时间字段的坑:审批记录显示的创建时间比实际早或晚 8 小时。这条链路上一共三个时区配置点,JDBC 连接串的serverTimezone、Jackson 序列化的time-zone、以及容器本身的TZ环境变量。三个都设成Asia/Shanghai就不会出问题,只改其中一两个反而更乱。这种问题查起来像玄学,但只要记住「一处不对,处处对不上」,按链路逐个核对就行。
6. 二次开发:把通用 OA 改成你自己的业务系统
这套代码真正值钱的地方不是功能有多全,而是它把「新增一个业务模块」的路径铺得很短。我拿请假单改造成设备报修单试过一次,从建表到流程跑通大概半天。
做法是四步。第一步建业务表,字段按报修场景定义,保留id、create_by、create_time、deleted这几个公共字段,MyBatis-Plus 的自动填充能直接用。第二步照抄请假单的 Controller、Service、Mapper 三层,把实体和 DTO 换掉,分页查询和详情接口几乎不用改。第三步在oa_flow_def里插一条flow_key = repair_apply的定义,node_config按报修审批链配好节点和处理人。第四步在菜单表里加一条记录,配上权限码,前端路由会自动挂出来,页面组件复制一份请假单的列表页改字段即可。
流程节点的扩展点在FlowNodeHandler这个接口上,不同处理人类型对应不同实现,新增一种「按设备所属部门找负责人」的规则,只要实现接口并注册成 Bean:
@Component public class DeptLeaderHandler implements FlowNodeHandler { @Resource private DeptMapper deptMapper; // 返回该节点实际需要生成待办的用户 id 列表 @Override public List<Long> resolveHandlers(FlowNode node, FlowInstance instance) { Long deptId = deptMapper.selectDeptIdByBizId(instance.getBizId()); if (deptId == null) { throw new BizException("单据未关联部门,无法确定审批人"); } Long leaderId = deptMapper.selectLeaderId(deptId); // 负责人为空时回退到管理员,避免流程卡死 return leaderId == null ? Collections.singletonList(1L) : Collections.singletonList(leaderId); } // 与 node_config 里的 handlerType 对应 @Override public String type() { return "deptLeader"; } }注册之后在node_config的 JSON 里把对应节点的handlerType写成deptLeader就能生效,不用改流程引擎的核心代码。这种「策略接口 + 类型字符串」的写法是我比较推荐的扩展方式,比在 Service 里堆if-else判断处理人类型要好维护得多,新人接手也能一眼看出扩展点在哪。
验证环节别偷懒。接口层用 curl 或者接口工具把新增、提交、审批、驳回、撤回五条路径各跑一遍;数据层直接查oa_flow_instance和oa_flow_task,确认状态和节点下标对得上;前端重点看列表页的权限码有没有配对,按钮该显示的显示、该隐藏的隐藏。改完流程定义记得把旧版本status置 0,不然同一个flow_key存在多条启用记录,提交时会随机命中一条,这种问题排查起来非常费劲。
我现在的习惯是,每接一套这类 OA 系统项目,都先把sql目录下的脚本、application.yml里的三个外部依赖、还有拦截器放行列表强制过一遍,再动手写第一行业务代码。这三处不出问题,后面基本不会卡在环境上。希望帮到你。
本文还有配套的精品资源,点击获取