news 2026/10/17 7:15:49

OA系统项目实战:Spring Boot+Vue权限模型与Flowable工作流设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OA系统项目实战:Spring Boot+Vue权限模型与Flowable工作流设计

简介:这份OA系统项目资源以办公自动化系统的完整项目实现为主线,适合正在学习Java Web开发、需要参考企业级权限与流程设计的学习者,也适合准备搭建内部OA的维护人员。压缩包共1212个文件,大小约17.92MB,主要包含Java与JSP后台逻辑、JS与CSS前端页面、XML配置文件、class编译产物及jar依赖库,另有一定数量的图片与HTML页面,整体目录结构便于按模块检索。内容完整覆盖需求分析、系统设计、流程引擎、权限管理、集成能力、移动办公、报表与数据分析、实施培训、后期维护与项目管理等OA项目关键环节,能够为理解企业审批、报销、请假等典型流程提供具体参考。已有385人浏览学习,通过学习该资源可快速获得一套可运行的OA项目骨架及后续二次开发的思路。

1. OA系统项目:审批流程线上化这件事,为什么比想象中难

一个五十人的贸易公司,老板最头疼的不是业务,而是报销单。员工贴好发票找部门主管签字,主管不在就等,财务月底对账发现单子没走完,行政还得拿Excel催。这还只是报销一条线,请假、用章、采购、合同审批全是类似流程。OA系统项目要解决的就是把这类“人找人签字”的流程搬到线上:发起、审批、驳回、转办、抄送、归档,每一步有记录,每个人只看到自己该看的。一个典型的OA系统项目,核心模块通常是组织架构管理、用户权限、审批工作流、消息通知和附件管理,技术栈最常见的是Spring Boot + Vue + MySQL。这个方向适合三类人:想给公司搭内部系统的后端工程师、接外包单的全栈开发者、以及刚学完Java想用完整项目练手的新人。别小看OA,权限模型和工作流引擎这两块,才是真正拉开项目质量差距的地方。

2. OA系统的技术选型:为什么约定俗成是Spring Boot + Vue这套组合

2.1 前后端分离是起点:OA系统项目的模块到底怎么切

OA系统的功能边界比大多数管理系统要宽。按我自己的习惯,先不急着写代码,把模块按“数据归属”切分成五块:组织权限(用户、部门、角色、菜单)、流程引擎(请假、报销、合同审批等所有走审批的业务)、消息中心(待办提醒、已办通知、系统公告)、附件服务(图片、Word、Excel、PDF的统一上传下载)、以及基础管理(操作日志、数据字典、定时任务)。这五个模块之间有一个明确的依赖方向:流程引擎和消息中心都依赖组织权限里的用户和部门数据,附件服务则被所有模块共用。切好模块之后,前后端分离就成了自然选择——Vue负责页面交互和路由控制,Spring Boot提供REST接口,双方用JSON通信。

前后端分离对OA项目还有一个实际好处:审批页面往往要嵌入表单、流程图、附件预览这些复杂组件,如果混在一个服务端渲染的模板里,改一处样式就可能影响整个流程页面,风险很高。分离之后,后端只关心业务逻辑和流程流转,前端只关心交互渲染,联调时用Swagger或者Postman先对齐接口格式,再写页面,效率比同步开发高很多。

2.2 核心依赖清单:Shiro、MyBatis-Plus、Flowable各自的职责边界

技术选型不是越多越好,而是每个依赖都解决一个“如果不引入它就得自己造轮子”的问题。这里列一份我常用的组合清单:

依赖职责不引入它的代价
Spring Boot应用框架,管理Bean生命周期、HTTP接口、自动配置手写容器,基本不可行
Apache Shiro / Spring Security认证与授权:登录、会话、角色判断、权限注解自己写拦截器和Session管理,容易漏掉并发和会话固定攻击
MyBatis-PlusORM,单表CRUD、分页查询、条件构造器纯MyBatis写大量重复SQL,OA的列表查询会有几十条近似的select
Flowable / Activiti工作流引擎:流程定义、任务分配、流程实例管理自研状态机,碰上驳回、会签、转办就是灾难
Redis缓存用户权限、验证码、待办计数每次都查数据库,权限接口在高并发下拖垮DB
MinIO / 阿里云OSS附件存储把文件存本地磁盘,服务器扩容或迁移时丢文件
Hutool工具库:日期、Excel导出、加密自己封装工具类,OA项目里这类代码会写几十个

Shiro和Flowable是整个OA系统的两个核心依赖。Shiro管“谁能进这个系统、能看哪些菜单”,Flowable管“这张审批单现在在谁手里、下一步去哪里”。两者的职责必须分清楚:Shiro不参与具体的流程节点判断,Flowable也不负责登录鉴权。实际操作中见过有人把审批人判断写在Shiro的过滤器里,结果流程多级审批时过滤器逻辑膨胀成一坨if-else,这是典型的职责混淆。

2.3 数据库设计定生死:用户、部门、角色、菜单四张核心表怎么建不返工

数据库设计在OA项目里有个显著特点:表和表之间的关系比普通管理系统更密集。用户属于部门,用户拥有角色,角色关联菜单,部门还有上下级层级。如果字段命名随意、关联关系不提前定好,做到工作流模块时就会反复改表结构。这里给出最核心的四张表建表SQL,也是我认为的最小可用版本。

CREATE TABLE sys_dept ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '部门ID', parent_id BIGINT DEFAULT 0 COMMENT '上级部门ID,0表示根节点', dept_name VARCHAR(50) NOT NULL COMMENT '部门名称', dept_code VARCHAR(30) COMMENT '部门编码,用于数据权限匹配', sort_order INT DEFAULT 0 COMMENT '同层级排序', status TINYINT DEFAULT 1 COMMENT '1启用 0停用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT '部门表'; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', dept_id BIGINT COMMENT '所属部门ID', username VARCHAR(30) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(100) NOT NULL COMMENT 'BCrypt加密后的密码', real_name VARCHAR(30) COMMENT '姓名', mobile VARCHAR(20) COMMENT '手机号', status TINYINT DEFAULT 1 COMMENT '1启用 0锁定', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '用户表'; CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_code VARCHAR(30) NOT NULL UNIQUE COMMENT '角色编码,如DEPT_MANAGER', role_name VARCHAR(50) NOT NULL COMMENT '角色名称', data_scope TINYINT DEFAULT 2 COMMENT '1全部 2本部门 3本部门及以下 4仅本人', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '角色表'; CREATE TABLE sys_menu ( id BIGINT PRIMARY KEY AUTO_INCREMENT, menu_name VARCHAR(50) NOT NULL COMMENT '菜单/按钮名称', parent_id BIGINT DEFAULT 0 COMMENT '上级菜单ID', menu_type TINYINT COMMENT '1目录 2菜单 3按钮', path VARCHAR(100) COMMENT '前端路由路径', perms VARCHAR(100) COMMENT '权限标识,如oa:leave:approve', icon VARCHAR(50) COMMENT '图标', sort_order INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '菜单权限表';

用户和角色的关联、角色和菜单的关联各需要一张关联表,加上上面四张,总共六张表构成权限模型的基础。部门表里设置parent_id自关联是为了做树形结构,前端渲染组织架构树时直接按parent_id递归加载。data_scope字段是数据权限的关键:它决定了一个角色能看哪些人的数据,这个字段后面在做列表接口时作用非常大,建议在选型阶段就定下来,不要等业务跑起来再补。

3. 权限模型:RBAC在OA系统里的落地细节与拦截器代码

3.1 为什么禁止在Controller里手写if判断角色

OA系统权限设计的第一个原则是:权限判断必须集中,不能散落在业务代码里。新手最常见的写法是在每个Controller里写这样的逻辑:if (user.getRole().equals("admin")) { ... }。表面看能跑,实际上问题很大。第一个问题是角色字符串硬编码在业务代码里,一旦角色编码从admin改成ADMIN,全项目要搜索替换;第二个问题是多个接口的权限逻辑容易不一致,比如A接口判断了管理员才能导出,B接口漏了判断,数据就泄露了;第三个问题是业务代码被权限逻辑穿插,后面维护的人根本分不清哪段是权限控制哪段是业务规则。

正确的做法是把权限判断放到两个层面:接口层面用注解或拦截器统一处理“能不能访问这个接口”,数据层面用SQL拼接处理“能访问哪些数据”。前者管功能权限,后者管数据权限,两者互不替代。

3.2 用注解+拦截器实现接口权限:一个可以直接抄的鉴权代码

我一般用Shiro加自定义注解的方式做接口鉴权。先定义一个权限注解:

@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface RequiresPermission { String value(); // 权限标识,对应 sys_menu.perms 字段 }

然后在Controller方法上标注需要的权限:

@RestController @RequestMapping("/api/leave") public class LeaveController { @PostMapping("/approve") @RequiresPermission("oa:leave:approve") public Result approve(@RequestBody LeaveApproveDTO dto) { // 审批逻辑,走到这里说明权限已通过 leaveService.approve(dto); return Result.success(); } }

最后在Shiro的过滤器链里加一个自定义过滤器,统一解析注解:

public class PermissionFilter extends AuthorizationFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { HandlerMethod handler = getHandlerMethod(request, response); if (handler == null) { return true; } RequiresPermission annotation = handler.getMethodAnnotation(RequiresPermission.class); if (annotation == null) { // 没有注解的接口,默认登录即可访问 return true; } Subject subject = getSubject(request, response); return subject.isPermitted(annotation.value()); } }

这段代码的核心逻辑是:从请求中解析出当前要调用的Controller方法,检查方法上有没有@RequiresPermission注解,有则取出权限标识,用Shiro的isPermitted判断当前用户是否拥有该权限。isPermitted的权限数据来自Realm里查询出的用户角色关联的菜单perms集合,也就是前面设计的sys_menu.perms字段。这里的参数annotation.value()就是权限标识字符串,命名规范建议用模块:功能:操作三段式,比如oa:leave:approve表示“办公模块,请假功能,审批操作”,这样在管理后台配置角色权限时一目了然。

3.3 数据权限:同一个列表接口怎么按角色过滤数据

功能权限解决的是“能不能点这个按钮”,数据权限解决的是“点了按钮之后看到哪些数据”。比如报销单列表,普通员工只能看自己提交的单子,部门主管能看本部门的单子,财务总监能看全公司的单子。如果每个列表接口都单独写SQL判断角色,工作量巨大且容易漏。常见的做法是MyBatis-Plus的拦截器做统一处理,在SQL执行前自动拼接数据权限条件。

@Component public class DataScopeInterceptor implements InnerInterceptor { @Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // 从当前登录上下文获取用户信息 LoginUser user = SecurityUtils.getLoginUser(); if (user == null) { return; } // 超级管理员不过滤 if (user.isAdmin()) { return; } // 根据用户所属角色得到数据范围 Integer dataScope = user.getDataScope(); String sql = boundSql.getSql(); String finalSql = sql; if (dataScope == 2) { // 仅本部门:拼接 dept_id = 当前用户部门 finalSql = sql + " AND dept_id = " + user.getDeptId(); } else if (dataScope == 3) { // 本部门及以下:拼接 dept_id IN (子孙部门) List<Long> deptIds = deptService.getChildDeptIds(user.getDeptId()); finalSql = sql + " AND dept_id IN (" + StringUtils.join(deptIds, ",") + ")"; } else if (dataScope == 4) { // 仅本人:拼接 create_by = 当前用户ID finalSql = sql + " AND create_by = " + user.getUserId(); } // 用拼接后的SQL替换原SQL boundSql.getSql(); // 实际使用中通过反射或MyBatis-Plus的JSqlParser修改SQL } }

注意一点:上面代码是简化示意,真正开发时直接用字符串拼接SQL有注入风险,更规范的做法是用MyBatis-Plus提供的JsqlParser解析原SQL,往WHERE条件里追加表达式。数据权限的难点在于“本部门及以下”要递归查子孙部门,这个操作建议加缓存,不然每个列表请求都要递归一次部门树。实际项目中,这个拦截器帮我把几十个列表接口的数据权限逻辑收敛到了一处,后续如果出现某个业务表需要单独控制,就在对应Mapper上跳过拦截器,只处理特殊的那个,整体维护成本低很多。

4. 工作流引擎:从自研状态机到Flowable,审批流程怎么建模

4.1 自研状态机的痛点是会签和驳回,不是简单流转

很多团队一开始觉得OA的审批不就是“提交→通过→结束”或者“提交→驳回→结束”,自己用状态字段加几张表就能实现。这个认知在只有一级审批时是对的,一旦流程变成“部门主管审批→财务复核→总经理审批”,且中间任何一级都可以驳回,还要支持会签(多个人同时审批)、或签(多个人任一通过即可)、转办(A转给B处理),自研状态机的状态数量会爆炸。比如请假流程四个节点,每个节点有“通过”“驳回”“转办”三个动作,状态流转路径就有几十条,每加一个节点就要改代码加枚举值。

工作流引擎的核心价值不是帮你省掉状态字段,而是把“流程定义”和“业务代码”分开。Flowable把流程定义成XML文件,节点、连线、条件、任务分配都在XML里描述,业务代码只负责在节点触发时执行对应的Service方法。这样审批流程的调整不用改Java代码,改XML然后重新部署流程定义就行。

4.2 一个请假审批的BPMN流程定义与部署代码

下面是一个最简的请假审批BPMN流程定义文件,包含两个审批节点:

<?xml version="1.0" encoding="UTF-8"?> <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" targetNamespace="http://flowable.org/bpmn"> <process id="leaveProcess" name="请假审批流程" isExecutable="true"> <startEvent id="startEvent" name="发起申请" /> <userTask id="managerApprove" name="部门主管审批" flowable:assignee="${managerUserId}"> <extensionElements> <flowable:formProperty id="approveResult" name="审批结果" type="string" required="true" /> </extensionElements> </userTask> <userTask id="hrConfirm" name="HR备案" flowable:assignee="${hrUserId}" /> <endEvent id="endEvent" name="结束" /> <sequenceFlow id="flow1" sourceRef="startEvent" targetRef="managerApprove" /> <sequenceFlow id="flow2" sourceRef="managerApprove" targetRef="hrConfirm"> <conditionExpression xsi:type="tFormalExpression"> <![CDATA[${approveResult == 'agree'}]]> </conditionExpression> </sequenceFlow> <sequenceFlow id="flow3" sourceRef="managerApprove" targetRef="endEvent"> <conditionExpression xsi:type="tFormalExpression"> <![CDATA[${approveResult == 'reject'}]]> </conditionExpression> </sequenceFlow> <sequenceFlow id="flow4" sourceRef="hrConfirm" targetRef="endEvent" /> </process> </definitions>

流程定义写好后,在Spring Boot项目里通过Flowable的RepositoryService部署:

@Service public class FlowableDeployService { @Autowired private RepositoryService repositoryService; public void deployLeaveProcess() { // 读取classpath下的bpmn文件 InputStream inputStream = getClass().getResourceAsStream("/processes/leaveProcess.bpmn"); // 部署流程定义,key为leaveProcess repositoryService.createDeployment() .addInputStream("leaveProcess.bpmn", inputStream) .name("请假审批流程") .deploy(); } }

部署之后,启动一个流程实例需要指定业务表单的关联ID和流程变量:

Map<String, Object> variables = new HashMap<>(); variables.put("managerUserId", deptManagerId); variables.put("hrUserId", hrUserId); variables.put("businessId", leaveId); ProcessInstance instance = runtimeService.startProcessInstanceByKey("leaveProcess", variables);

这里startProcessInstanceByKey的第二个参数是业务键,我一般把业务表的ID传进去,这样能在act_ru_identitylink表里反查业务单据。流程变量managerUserId和hrUserId分别指定了部门主管审批和HR备案的处理人,这是Flowable的flowable:assignee表达式从流程变量里取值的用法。常见错误是把这两个变量放在启动流程之后才设置,这会导致任务创建时找不到处理人,任务卡在待办里没人能查得到。

4.3 会签、或签、转办的配置套路

这三个是OA审批里的高频场景,Flowable都支持,但配置方式不同。会签使用多实例节点,在BPMN中给userTask加multiInstanceLoopCharacteristics:

<userTask id="multiApprove" name="多人会签" flowable:assignee="${assignee}"> <multiInstanceLoopCharacteristics isSequential="false" flowable:collection="assigneeList" flowable:elementVariable="assignee"> <completionCondition>${nrOfCompletedInstances >= nrOfInstances / 2 + 1}</completionCondition> </multiInstanceLoopCharacteristics> </userTask>

这个配置的含义是:assigneeList是一个流程变量,存放所有需要参与会签的人的用户ID列表,引擎会为列表里的每个人生成一个独立的任务实例。completionCondition指定了完成条件,上面这个表达式表示完成超过一半就算通过,实际项目里会签通常要求全部人通过,那就把条件改成${nrOfCompletedInstances == nrOfInstances}。或签不需要多实例,只需要在任务节点的flowable:assignee表达式里传入一个候选组,使用flowable:candidateUsers指定候选人列表,一个人处理完后任务就结束。

转办的实现则相对独立,它不涉及流程定义,而是在运行时把任务指派人改掉:

taskService.setAssignee(taskId, targetUserId);

转办是OA系统里容易踩坑的地方,因为原处理人转办之后就不再能看到这个任务,如果转办人和接收人沟通不畅,流程会停滞在接收人手里,超时之后需要管理员的“强制撤回”权限才能把任务拿回来。因此在做转办功能时,前端一定要显示转办前后的处理人和时间,后端要记录操作日志,这是交付验收时审计必须的痕迹。

5. OA系统部署与运行:从开发机到服务器的避坑指南

5.1 权限缓存不刷新:改了角色权限,用户刷新后还是旧菜单

现象:管理员在后台给某角色增加了“合同审批”菜单权限,用户退出重新登录后菜单没有变化,过了很久才生效。

原因:登录成功后用户的权限集合被放进了Redis缓存,缓存key通常是login:user:permissions:{userId},退出登录时没有主动删除缓存,用户再次登录时走了缓存直接返回了旧的权限列表。Shiro的isPermitted查询优先查缓存,查不到才调Realm,所以修改权限后必须手动清缓存。

解决:在角色权限变更的Service方法里,找到所有拥有该角色的用户,逐一删除权限缓存key。注意不要用flushdb清整个Redis,那会把登录态和数据缓存全部冲掉,正确做法是精确删除:

public void updateRolePermissions(Long roleId, List<Long> menuIds) { // 更新角色菜单关联表 roleMenuService.updateRoleMenu(roleId, menuIds); // 查出该角色关联的所有用户 List<Long> userIds = userRoleService.getUserIdsByRoleId(roleId); // 删除这些用户的权限缓存 for (Long userId : userIds) { String key = "login:user:permissions:" + userId; redisTemplate.delete(key); } }

这种做法能保证权限变更后,用户下次刷新接口时重新走Realm加载新的权限数据。还有一个容易被忽略的细节:如果权限缓存里存的是菜单列表,改了菜单名或图标,同样要走这个逻辑。把所有“修改角色权限”的入口都集中到一个Service方法,是规避这个坑最有效的办法。

5.2 流程实例卡死:提交了审批单,待办列表里查不到

现象:用户在系统里提交了请假申请,界面上显示“已提交”,但审批人登录后待办列表为空。去数据库查流程实例,发现流程实例是激活状态,任务却没有对应的处理人。

原因:这个坑多半出在流程变量的赋值时机上。前面提到过,Flowable创建第一个任务节点时需要从流程变量里解析assignee表达式,如果启动流程实例时managerUserId为空,引擎会创建一个没有处理人的任务。在Flowable的表结构里,对应任务行的assignee_字段是NULL,待办查询的SQL是WHERE assignee_ = #{userId},自然查不到。

解决:排查这类问题时,先通过act_hi_procinst表找到流程实例ID,再查act_ru_task表看assignee_字段是否为空。如果为空,处理方式有两种:一种是直接补设处理人并提醒用户重新提交;另一种是在代码层面加保护,启动流程实例前校验所有assignee变量非空:

Map<String, Object> variables = buildProcessVariables(leaveForm); // 校验审批人是否已设置 if (variables.get("managerUserId") == null) { throw new BusinessException("未找到部门主管,请先维护部门负责人"); }

这里给审批人变量做了空值保护,避免任务卡在无人处理的状态。生产环境最好在提交申请前从前端就校验一次,后端再校验一次,两道防线才能避免脏数据流入流程引擎。

5.3 定时任务时区问题:测试环境正常,服务器上却不执行

现象:OA系统里有一个“每日早上9点发送待办提醒”的定时任务,在本地开发环境一切正常,部署到服务器之后到点不执行。查日志发现定时任务压根没被触发。

原因:服务器系统时区不是Asia/Shanghai,或者Spring Boot应用没有显式指定时区。Linux服务器默认可能是UTC时间,早上9点的cron表达式在UTC下对应的是北京时间下午5点,用户当然看不到提醒。另一个隐藏原因是服务器上跑了多个Java应用,每个应用都设置了不同的user.timezone,导致同一个@Scheduled注解在不同环境触发时间不一致。

解决:在Spring Boot配置类里显式设置时区,并在cron表达式上注明时区含义:

@Configuration public class ScheduledConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { // 设置任务调度器使用的时区为东八区 taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10)); taskRegistrar.getScheduler().setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); } }

同时,在application.yml里加上spring.jackson.time-zone: Asia/Shanghai,保证接口返回的时间字符串不带时区歧义。定时任务的线程池大小也要关注,OA系统里定时任务通常有:待办提醒、会话清理、附件临时文件清理。如果所有任务都共用一个默认的单线程调度器,某一个任务执行时间过长(比如推送大量消息),其他任务会排队延迟。给调度器单独建一个合理大小的线程池,是避免“任务互相拖垮”的关键。

5.4 附件存储路径:上传成功但访问404,问题出在路径拼接

现象:用户在报销单里上传了发票照片,界面提示上传成功,但点击预览时图片加载不出来,浏览器直接404。检查服务器磁盘发现文件确实写入了。

原因:前端请求的图片URL和后端实际存储路径不一致。常见的情况是,开发环境后端把附件存在了项目运行目录下的/upload/,nginx配置的静态资源路径是/data/oa/upload/,代理到后端时URL前缀没对齐。另一个常见情况是,Windows开发环境路径分隔符是\,Linux是/,直接把本地路径拼到URL里就变成了/upload\2024\01\xxx.jpg,浏览器肯定打不开。

解决:所有附件的访问不要直接暴露物理路径,统一走附件下载接口:

@GetMapping("/file/preview/{fileId}") public ResponseEntity<Resource> preview(@PathVariable Long fileId) { // 从数据库查出文件元信息,得到存储路径 FileMeta meta = fileMetaService.getById(fileId); // 禁止前端传路径,只传文件ID,避免路径穿越攻击 Path path = Paths.get(meta.getStoragePath()); Resource resource = new FileSystemResource(path); String contentType = Files.probeContentType(path); return ResponseEntity.ok() .contentType(MediaType.parseMediaType(contentType)) .body(resource); }

接口层只接收文件ID,不接收路径参数,这样既能统一处理URL前缀,又能防止用户恶意传入../../etc/passwd之类的路径穿越。nginx配置里把这个接口的反向代理配好,前端永远只拼/file/preview/{id}这个固定前缀,彻底避开路径分隔符和部署目录不一致的问题。上传后的临时文件清理任务也要注意,清理机制一般约定“只清理超过72小时且没有被任何业务单据关联的文件”,避免误删刚上传还没提交表单的图片。

6. 把OA系统打磨到能交付验收:压测关注点与数据归档策略

6.1 JMeter压测登录与待办列表:盯住两个阈值

交付前压测是很多团队跳过的一步,但OA系统并发一上来就有两类接口最容易出问题:登录接口和待办列表接口。登录接口的问题在于密码加密和会话创建的耗时,待办列表的问题在于多表关联查询。用JMeter建一个最简单的测试计划:线程组100个用户、循环5次、添加两个HTTP请求,一个POST到登录接口,一个GET到待办列表接口。压测观察两个指标:接口响应时间的P95是否超过500毫秒,以及错误率是否超过1%。

如果登录接口慢,优先检查Redis连接池配置和BCrypt加密强度。BCrypt的强度参数cost默认是10,调成12之后单次加密耗时可能从几十毫秒涨到几百毫秒,并发一高直接拖垮登录。如果待办列表慢,优先看SQL有没有扫全表,Flowable的act_ru_task表在数据量大之后必须建立assignee_和proc_inst_id_的联合索引,这一步不做,待办查询就是全表扫描。

6.2 历史数据归档:审批表不能无限增长

OA系统运行一年后,act_hi_procinst(历史流程实例)和act_hi_taskinst(历史任务)两张表的数据量会增长得非常快。每个审批节点都会留下一行历史任务记录,一个走完五级审批的报销单会产生五条历史任务数据。两三年下来,这几张表能膨胀到几千万行,导致流程引擎的查询越来越慢。

归档策略我一般建议按年做分区或者定期迁移到历史库。最简单的做法是写一个归档定时任务,把一年前的已完成流程实例,从act_hi_*表迁移到act_hi_*_archive备份表,迁移完成后删除原表记录。注意不要删除运行中的流程实例,判断条件要同时加上END_TIME_ IS NOT NULL和END_TIME_ < 归档截止时间。归档任务要避开业务高峰,建议凌晨执行。

交付验收时还有一张检查清单值得过一遍:所有列表接口是否都做了分页,Excel导出是否用了异步任务而不是同步导出导致超时,密码是否全部使用BCrypt存储,附件是否有类型白名单校验,操作日志是否记录了谁在什么时间审批了什么单据。这些点逐个确认过,OA系统才算是具备基本的生产可用水平,而不是一个能跑demo的作品。

整个OA系统项目做下来,我最大的感受是:技术选型不是最难的部分,真正考验人的是对权限边界和流程状态的把控。权限模型设计得不严谨,后续每个新功能都在给权限代码打补丁;流程变量没有做空值保护,审批单就会悄无声息地卡住。这些坑都是在真实项目里付过代价换来的,如果你正在做的OA系统也碰到了类似问题,按上面这几条逐一排查,能省下不少排查时间。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 7:15:28

分布式光伏配电网无功电压分层协调控制与Matlab仿真

分布式光伏大规模接入之后&#xff0c;配电网的电压问题把我折磨得不轻。白天光照强、负荷轻的时候&#xff0c;馈线末端电压一路飙升&#xff0c;逆潮流直接把变电站出口的自动电压调节器搞到不知所措&#xff1b;傍晚负荷上来、光照减弱&#xff0c;电压又断崖式跌落。传统思…

作者头像 李华
网站建设 2026/10/17 7:15:28

Windows版Redis后台启动完全指南:从黑窗口到系统服务

在很多开发者眼里&#xff0c;Windows 上的 Redis 就是一个“临时玩具”&#xff1a;下载解压&#xff0c;双击 redis-server.exe&#xff0c;弹出一个黑窗口&#xff0c;能用&#xff0c;但总感觉不踏实——窗口一关 Redis 就没了&#xff0c;电脑一重启还得手动再开一遍。今天…

作者头像 李华
网站建设 2026/10/17 7:13:38

logviewer:高性能终端日志查看器实战指南

简介&#xff1a;这是一款面向开发、运维及测试人员的轻量级日志实时查看与分析工具——LogViewer Pro&#xff0c;专为高效处理超大体积日志文件设计&#xff0c;实测可稳定加载并滚动浏览46GB级别日志&#xff0c;显著缓解传统文本编辑器在打开大型log时的卡顿与崩溃问题。资…

作者头像 李华
网站建设 2026/10/17 7:13:37

2026 AIGC检测原理与降AI实战:从工具到手写流程全拆解

2026年又把一批人拦在了门外。打开提交页面&#xff0c;红色提示“AI疑似度85%”&#xff0c;整个人都不好了。这个场景我太熟了&#xff0c;上半年几乎所有写论文、写汇报、写公众号的人都在经历同一件事&#xff1a;身边朋友写的稿子一遍过&#xff0c;自己辛辛苦苦改了半天&…

作者头像 李华
网站建设 2026/10/17 7:13:00

exbsa提取工具实战:从容器结构解析到批量提取与解码插件开发

简介&#xff1a;ExBSA提取工具是一款面向BISHOP社GALGAME爱好者与开发者的解包软件&#xff0c;用于解析游戏中的BSA&#xff08;BISHOP Archive&#xff09;打包档案&#xff0c;提取剧本、图片、音乐等素材&#xff0c;便于分析、修改与备份。资源包共6个文件&#xff0c;以…

作者头像 李华
网站建设 2026/10/17 7:12:07

AI报告审核系统IACheck:节能报告智能校验与质量成本双优化

做企业节能管理这些年&#xff0c;最磨人的不是设备改造&#xff0c;也不是能耗数据采集&#xff0c;而是每个季度末那份要命的节能报告审核。几十份报告、几百个数据&#xff0c;既要看格式&#xff0c;又要算指标&#xff0c;还要对标准&#xff0c;人工翻下来眼睛发花&#…

作者头像 李华