1. 项目背景与核心需求拆解
这几年行政审批类系统一直是计算机毕业设计的热门方向,原因其实很简单:业务逻辑清晰、技术栈主流、展示效果好,而且答辩时老师能一眼看出系统解决的是什么问题。但热门归热门,真正能把审批流转做明白的毕业设计并不多。很多同学拿到的题目是"行政审批系统",但做出来要么是一个简单的表单增删改查,要么是硬套工作流引擎结果自己都讲不清流程怎么走的。所以这篇文章想聊的,就是怎么用SpringBoot从零搭建一个面向机关单位的轻量化审批流转平台。
先说清楚这个系统是什么:它解决的是"申请-审批-归档"这条业务链路的在线化问题。过去纸质审批需要申请人跑多个部门、找多位领导签字,现在通过系统提交申请,审批人在线处理,每一步流转都有迹可循。从技术角度看,它本质上是一个带状态流转的业务管理系统,核心难点在于流程控制、表单复用、权限隔离这三件事,而不是简单的CRUD。
我见过不少同学拿到这类题目后的第一反应是找RuoYi、若依这类脚手架直接改,或者集成Activity工作流引擎扛大梁。这个思路不能说错,但对毕业设计来说,如果集成Activity,答辩时老师问"你讲讲Activity的流程部署和表结构",答不上来的话反而扣分。而且Activity本身的表结构有几十张,光是理解它的事件机制和分析表关系就要花掉大量时间。我个人的建议是:毕业设计场景下,在业务表的层面自己实现轻量级审批状态机,把流程节点和流转记录用代码控制清楚,反而比套一个重型引擎更能体现你对业务的理解深度。
再说说题目里的"轻量化"三个字,它是这个项目区别于企业级中台产品的关键定位。机关单位的审批流通常有固定的层级关系,比如"申请->科室初审->分管领导复审->办结",层级不多,节点固定,并发量也不高。这种场景下用一个合理设计的业务表加上状态枚举,足够支撑几百人的日常办公需求。相比引入工作流引擎、规则引擎这些重武器,轻量化的实现思路是"每一笔审批单的状态都明确记录在数据库里,每一步流转都由代码显式控制",这样代码可读性高,出问题容易排查,也不会在答辩时被问到分布式事务、流程实例持久化这类难以招架的问题。
适合参考这个项目的读者,一是正在选毕业设计题目、想找"既有业务深度又不过度复杂"题目的同学,二是刚入门SpringBoot、想理解一个完整业务系统如何组织代码的同学,三是对政务类系统的权限模型和流程设计感兴趣、想快速上手一套可演示原型的开发者。接下来我按实际开发的顺序,从技术选型、模块设计、数据库建模、权限模型、核心流程实现到踩坑实录,完整走一遍。
2. 技术选型与架构设计思路
2.1 为什么最终选了SpringBoot单体架构
行政审批系统的用户量级和使用场景决定了技术选型不需要过度设计。机关单位内部使用,通常只有几十到几百个账号,日申请单量在几百条以内,这样一个典型的中小型业务系统,单体架构是最合理的选择。SpringBoot的好处在于内置Tomcat、简化依赖管理、生态成熟,配合Maven一键构建,部署也只是打一个jar包的事,对毕业设计来说,这些特性可以省掉大量环境折腾的时间。
不选择微服务架构的原因很简单:这个系统根本没有微服务拆分的业务基础。政务审批的申请、审批、用户、通知这些模块都共享同一套数据,强行拆成多个服务反而会引入服务间调用、数据一致性、分布式事务这些和审批业务无关的复杂度。如果你在答辩时主动解释"之所以不用微服务,是因为业务规模和团队维护成本决定了单体更合适",这比堆砌一堆微服务名词要给力得多。
前端我建议采用Thymeleaf服务端渲染加Bootstrap,或者Vue3加Element Plus分离式开发。前者适合一个人快速搞定,后者界面更现代、交互更流畅。实际项目中我见过两种方案都有人用,如果时间充裕且熟悉JavaScript,推荐前后端分离,如果后端还没完全搞明白,Thymeleaf渲染可以避免跨域、鉴权等前端联调问题。这里不再纠结,看个人技术储备做选择。
2.2 核心依赖清单与版本选择
基于SpringBoot 2.7.x版本(选择2.7而非3.x,原因是3.x要求JDK17,部分学校机器环境还停留在JDK8,兼容性考虑优先),核心依赖清单如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.31</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>这里有一个关键选择要说清楚:数据库访问层用MyBatis-Plus而不是纯MyBatis,也不是Spring Data JPA。原因有三个:第一,MyBatis-Plus提供了BaseMapper内置CRUD,手写SQL量大幅减少;第二,它的LambdaQueryWrapper让条件查询代码非常紧凑,审批列表的查询条件组合很灵活;第三,代码生成器可以直接根据表结构生成实体和Mapper,适合一个人快速搭项目。Spring Data JPA虽然也省事,但复杂查询时JPQL的手感和调试成本并没有比SQL好,而且国内公司用MyBatis系更多,对找工作也友好一些。安全框架则选择Spring Security,虽然配置比Shiro繁琐一点,但掌握了过滤器链机制后,权限控制能做得更细致,也更能体现技术深度。
2.3 目录结构与分层设计
我见过很多毕业设计代码把Controller里写满SQL,Service层形同虚设,这种代码即使能运行,答辩时也很难自圆其说。这个项目的分层遵循常规的Controller-Service-Mapper三层架构,但针对审批业务做了稍微细化的组织:
src/main/java/com/example/approval/ ├── controller/ // 接口层,只做参数接收和结果返回 │ ├── AuthController.java │ ├── ApprovalController.java │ ├── ApplicationController.java │ └── UserController.java ├── service/ // 业务逻辑层,流程流转的核心代码在这里 │ ├── ApprovalFlowService.java │ ├── ApplicationService.java │ └── UserService.java ├── mapper/ // 数据访问层,继承BaseMapper ├── entity/ // 数据库实体 ├── dto/ // 请求与响应对象,避免实体直接暴露 ├── common/ // 统一返回结果、异常处理、常量类 └── config/ // Security配置、MyBatis-Plus配置等分层设计的核心初衷,是把"流程如何流转"这种核心业务逻辑和"数据如何存取"这种技术细节分开。Controller只负责接参数和校验门面,流程的每一步都在Service中显式调用,这样当流程出错时,你可以顺着Service代码一行一行走查,而不是在整个项目里东翻西找。
3. 数据库设计与审批状态机建模
3.1 关键数据表结构设计
行政审批系统的数据库表设计是整篇文章里最值得花时间讲的部分,因为它直接决定业务流程能不能顺畅流转。完整的系统至少需要这些表:用户表、角色表、权限表、部门表、审批事项类型表(对应"哪些事项可以在线申请")、申请单主表(对应"谁提交了哪个事项的申请")、审批记录表(对应"每一步谁在什么时候批了什么意见")、附件表、通知消息表。
这里重点拆解申请单主表和审批记录表的设计思路。申请单主表不能只存申请人和申请事项,还要包含当前所处节点、申请单状态、表单内容这几类核心信息。推荐结构如下:
CREATE TABLE application ( id BIGINT PRIMARY KEY AUTO_INCREMENT, application_no VARCHAR(32) NOT NULL COMMENT '申请单编号', applicant_id BIGINT NOT NULL COMMENT '申请人ID', item_type_id BIGINT NOT NULL COMMENT '审批事项类型ID', form_data JSON COMMENT '动态表单数据', current_node INT NOT NULL DEFAULT 1 COMMENT '当前流程节点:1初审,2复审,3终审', status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0审批中,1已通过,2已驳回,3已撤回', submit_time DATETIME NOT NULL, finish_time DATETIME NULL, UNIQUE KEY uk_application_no (application_no) ) COMMENT='申请单主表';form_data字段用JSON类型存储是很多同学容易忽略的亮点。审批事项五花八门,比如"会议室申请"需要填时间段、"用章申请"需要填文件标题、"请假申请"需要填天数,如果每一种事项建一张表,系统就没法扩展了。用JSON字段存储动态表单数据,事项类型表只定义"表单模板长什么样",申请时按模板渲染表单,提交后数据序列化到form_data,这样一套表结构可以支撑任意类型的审批事项。
审批记录表则是流程追溯的核心。每一笔审批动作都要插入一条记录,包含申请单ID、审批人ID、审批节点、审批动作(同意/驳回/转办)、审批意见、审批时间。这张表本质上就是审批流的"日志账本",不要想着只更新主表的状态字段就完事,没有明细记录的审批系统是站不住脚的。
3.2 轻量级状态机的核心设计逻辑
审批流转其实就是一个状态机:状态节点是固定的几级,触发事件是审批动作,转换条件是角色和操作。前面提到不用工作流引擎,那就用枚举和代码显式管理这个状态机。状态节点用枚举定义,比魔法数字可维护得多:
public enum FlowNode { DRAFT(0, "起草"), DEPT_REVIEW(1, "科室初审"), LEADER_REVIEW(2, "分管领导复审"), FINAL_REVIEW(3, "主要负责人终审"), DONE(4, "办结"); }流转规则用一张节点配置表来描述,说明每个节点可以执行什么动作、通过后进入哪个节点、驳回后回到哪个节点。示例设计如下:
| 当前节点 | 审批角色 | 通过后去向 | 驳回后去向 |
|---|---|---|---|
| 科室初审 | 科室负责人 | 分管领导复审 | 起草(申请人可修改) |
| 分管领导复审 | 分管领导 | 主要负责人终审 | 科室初审 |
| 主要负责人终审 | 主要负责人 | 办结 | 分管领导复审 |
这样设计以后,流程的每一步都是"拿着当前节点和审批动作,查配置表得到下一个节点"的确定性计算,代码写起来就是一个switch或一个Map查询。好处是流程逻辑显式可见,不像工作流引擎那样"流程跑在哪"需要查引擎的上下文表。轻量级的代价是流程节点一旦定死,运行期不能动态改流程模板,但对机关单位的固定审批流程来说,这个代价完全可以接受。
3.3 动态表单模板的设计方案
动态表单是政务审批系统区别于普通表单系统的核心特性。我建议用"事项类型表定义模板,前端渲染模板,后端存JSON数据"的三段式方案。事项类型表保存一张表单模板JSON,例如会议室申请事项的模板是这样的:
{ "fields": [ {"key": "meeting_room", "label": "会议室名称", "type": "input", "required": true}, {"key": "meeting_date", "label": "使用日期", "type": "date", "required": true}, {"key": "start_time", "label": "开始时间", "type": "time", "required": true}, {"key": "end_time", "label": "结束时间", "type": "time", "required": true}, {"key": "remark", "label": "备注", "type": "textarea", "required": false} ] }申请人在前端选择某个审批事项后,系统读取这个JSON模板,动态渲染出对应的表单控件。提交时前端收集表单数据,形成键值对JSON,随申请单一起保存到application表的form_data字段。审批人打开待办详情页时,读取form_data并配合模板还原出每一项字段和值的对应关系,就能看到"会议室名称:第一会议室"这样的人类可读信息。
这套方案把"表单字段"变成了数据而不是硬编码,新增一个审批事项只需要在后台配置一条模板记录,不需要改代码、不需要改数据库表结构。在答辩演示时,现场定义一个"办公用品领用申请"事项,马上就能提交申请、走审批,这种动态扩展能力的演示效果比任何图表PPT都更有说服力。
4. 权限模型与多级审批流程实现
4.1 RBAC权限模型的落地方式
行政审批系统天然有多角色、多部门、多层级的权限需求,上一篇文章里我详细拆解过RBAC的六张核心表,这里再结合审批场景说一下落地的思路。用户表关联部门,用户通过用户-角色关联表绑定角色,角色通过角色-权限关联表绑定权限,这是标准RBAC。审批系统里真正的难点在于"数据范围"的控制,而不只是"功能权限"的控制。
什么是数据范围?举个例子:科室负责人角色既能审批本科室成员的申请,也能查看本科室的历史申请记录,但不应看到其他科室的申请。同一个"审批"功能权限,数据可见范围却受部门隔离。这个场景用简单的菜单权限或接口权限是解决不了的,必须在查询申请单列表时动态拼接部门过滤条件。实现方式是在Security的登录用户信息里保存userId和deptId,查询列表时根据当前用户的角色判断是"只看本部门"还是"看全部",然后往MyBatis-Plus的QueryWrapper上追加eq条件。
这里推荐一个"前缀匹配法"处理部门层级:部门表增加parent_id字段,再用一个path字段存储从根部门到当前部门的路径,例如"1_3_7"。查询某个部门及其子部门时,用like '1_3_7%'直接匹配,避免递归查询子部门的性能损耗。机关单位常见的"某处包含几个科",用这个方案处理数据权限的部门树逻辑会很干净。
4.2 提交申请到办结的完整流程代码走查
核心流程从申请人提交申请单开始。我来完整走一遍代码逻辑,这段代码是整个系统的心脏。
@Service public class ApplicationServiceImpl implements ApplicationService { @Override @Transactional(rollbackFor = Exception.class) public void submitApplication(ApplicationSubmitDTO dto, LoginUser user) { // 1. 生成唯一申请单编号 String applicationNo = "AP" + System.currentTimeMillis() + RandomStringUtils.randomNumeric(4); // 2. 构造申请单主体 Application application = new Application(); application.setApplicationNo(applicationNo); application.setApplicantId(user.getUserId()); application.setItemTypeId(dto.getItemTypeId()); application.setFormData(dto.getFormDataJson()); application.setCurrentNode(FlowNode.DEPT_REVIEW.getCode()); application.setStatus(ApplicationStatus.PENDING.getCode()); application.setSubmitTime(LocalDateTime.now()); applicationMapper.insert(application); // 3. 写入审批记录 ApprovalRecord record = new ApprovalRecord(); record.setApplicationId(application.getId()); record.setAction(RecordAction.SUBMIT.getCode()); record.setOperatorId(user.getUserId()); record.setNodeCode(FlowNode.DEPT_REVIEW.getCode()); record.setComment("提交申请"); approvalRecordMapper.insert(record); // 4. 通知第一个审批节点的角色用户 notifyService.notifyPendingApproval(application.getId(), FlowNode.DEPT_REVIEW.getCode()); } }核心要点在事务注解。插入申请单和写入审批记录必须是一个原子操作,如果申请单插入成功但审批记录写入失败,会导致一笔申请"凭空出现但查不到来源",这在审批系统里是不能接受的。Spring的@Transactional在这里起到关键作用,默认回滚RuntimeException,所以Service里如果出现校验失败的情况,建议抛出业务异常而不是返回false然后手动判断。
审批动作的代码思路类似,核心是"判断当前用户是否有权审批此单"和"根据动作计算下一节点"。以科室初审通过为例:
@Override @Transactional(rollbackFor = Exception.class) public void approve(Long applicationId, String comment, LoginUser user) { Application application = applicationMapper.selectById(applicationId); // 校验当前节点是否匹配 if (!application.getCurrentNode().equals(FlowNode.DEPT_REVIEW.getCode())) { throw new BizException("当前申请单不处于科室初审节点,无法审批"); } // 校验审批人是否有权限:属于该申请单部门并具有审批角色 if (!approvalPermissionService.canApprove(application, user)) { throw new BizException("当前用户无权审批该申请单"); } // 更新主表节点状态 application.setCurrentNode(FlowNode.LEADER_REVIEW.getCode()); applicationMapper.updateById(application); // 写入审批记录 ApprovalRecord record = new ApprovalRecord(); record.setApplicationId(applicationId); record.setAction(RecordAction.APPROVE.getCode()); record.setOperatorId(user.getUserId()); record.setNodeCode(FlowNode.DEPT_REVIEW.getCode()); record.setComment(comment); approvalRecordMapper.insert(record); // 通知下一节点审批人 notifyService.notifyPendingApproval(applicationId, FlowNode.LEADER_REVIEW.getCode()); }这段代码里最容易被忽略的是"校验当前节点是否匹配"。如果不做这个校验,两个审批人同时打开待办列表,A先点了通过,B再点时就会把一个已经流转走的申请单再次审批,造成节点跳变。用current_node字段做乐观锁式的状态判断,配合更新时的WHERE current_node = ?条件(MyBatis-Plus的updatewrapper可带条件),就能防止并发重复审批。这一步是我强烈建议在答辩时主动讲出来的亮点,因为很多同学做审批系统根本没考虑并发。
4.3 驳回、撤回与办结的边界处理
审批不可能只有"通过"这一个动作。驳回和撤回的边界处理是流程设计里最容易被想当然的部分。
驳回的设计考虑是"驳回到哪一层"。很多初版系统把驳回一律打回起草节点,看似简单,但会给申请人带来极大的重复劳动:科室初审发现材料少盖了一个章,整单被驳回到起草状态,所有字段要重新填一遍,体验非常差。推荐的设计是"逐级驳回":每个节点配置的驳回目标节点通常是它的上一级,例如分管领导复审驳回到科室初审,科室初审只是需要补充材料时则驳回到起草节点。这样审批人可以灵活选择驳回到上一节点还是退回申请人,避免大量重复填报。
撤回逻辑也要想清楚。申请人提交后如果发现填错了,应当允许撤回,但撤回的起作用范围只限于"当前还没有任何审批人处理"或"仍在科室初审节点"。一旦第一级审批人已经处理过,申请单进入下一节点,申请人就不能单方面撤回了,否则审批人已经批过的单子突然消失,流程对账就对不上了。实现时只要判断current_node等于DEPT_REVIEW且还没有非提交动作的审批记录即可。
办结则是终点态,终审节点通过后更新主表状态为已通过并写入finish_time,同时给申请人发站内通知,整个流程结束。办结后的申请单数据原则上不可修改,只能查看和导出归档。
4.4 待办列表与已办列表的查询性能优化
机关单位几百个用户使用,待办列表的查询压力不大,但设计时仍然要注意索引和查询条件优化。待办列表的查询SQL要筛选出"当前节点等于某角色对应节点、状态为审批中、没有被当前用户处理过"的申请单。最容易出现的性能坑是在current_node、status、submit_time这些字段上没建索引,数据量上万条后列表页响应明显变慢。建议在application表上建组合索引(current_node, status),再配合submit_time降序排序,实测这个组合在几万条数据量下表现优秀。
待办列表涉及的另一个复杂点是"哪些用户能看到这个待办"。我推荐用一个"待办推送表"(task表)来优化:审批流转的每一步,系统除了更新主表节点,还往待办表插入一条记录,字段包括申请单ID、受理人ID、节点代码、状态。查询某人的待办列表就变成了查这张task表WHERE user_id = ? AND status = 0,简单直接且快。审批完成时更新对应task记录为已处理。这个设计的额外好处是后续如果需要做"已办归档"统计,直接查task表就能知道某人处理过多少笔申请,比反查审批记录表再做关联要高效得多。
5. 核心功能模块落地与实操要点
5.1 统一登录认证与安全退出
登录认证模块是行政审批系统的门面,机关单位对这个模块的第一印象往往决定了整个系统的评价。我用Spring Security实现表单登录,核心配置如下:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/login", "/css/**", "/js/**").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/approval/**").hasAnyRole("APPROVER", "ADMIN") .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .defaultSuccessUrl("/index") .permitAll() .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/login"); } }密码加密用BCryptPasswordEncoder是必须的,明文密码存储这种低级错误在答辩时一旦被问到"用户密码安全怎么保证"就完全被动了。BCrypt的hash自带盐值,同一密码两次加密结果不同,但matches方法可以校验,这是它比MD5加盐更优的原因。
还需要自定义UserDetailsService从数据库加载用户,并封装LoginUser对象携带userId和deptId。这里有个细节:UserDetails里保存userId和deptId,是为了在Service层直接获取当前操作人信息,而不需要在每次请求时重新查库。很多教程只演示了用户名和密码的认证,却忽略了业务系统里"当前操作人是谁"这个关键上下文。
5.2 审批中心列表、详情与附件管理
申请人端和审批人端都离不开申请单列表。列表页需要支持按申请单编号、事项类型、申请时间范围、状态做条件筛选。MyBatis-Plus的LambdaQueryWrapper在这里用起来非常顺手:
LambdaQueryWrapper<Application> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.hasText(dto.getApplicationNo()), Application::getApplicationNo, dto.getApplicationNo()) .eq(dto.getItemTypeId() != null, Application::getItemTypeId, dto.getItemTypeId()) .like(StringUtils.hasText(dto.getApplicantName()), Application::getApplicantName, dto.getApplicantName()) .eq(dto.getStatus() != null, Application::getStatus, dto.getStatus()) .orderByDesc(Application::getSubmitTime);列表查询尽量用DTO接收查询参数,再把DTO转成QueryWrapper条件,避免前端直接传一堆松散参数导致Service里出现大量if拼接。
详情页除了展示主表信息和表单数据,还要展示审批流转的完整轨迹。实现是按申请单ID查询审批记录表,按时间正序输出,每一行显示"【科室初审】张三 同意:材料齐全"这样的记录。这个"轨迹时间线"功能是整个系统最有说服力的环节,因为它证明了每一步操作都留痕。为了方便前端渲染轨迹,我通常在Service层把审批记录连同事项类型模板一起组织成一个VO返回,让前端拿到的是组装好的展示数据,而不是去自己拼业务逻辑。
附件管理也是审批单的重要组成部分。机关单位常见的申请材料是扫描件或图片,上传下载用本地磁盘存储就够用。附件表字段设计为:id、application_id、file_name、file_path、upload_time、uploader_id。需要注意上传目录的路径不要写死绝对路径,建议用配置文件管理,并在下载时做权限校验——只有申请人和与申请单相关的审批人可以下载附件。文件大小限制建议在配置里设为10MB,防止超大文件打满磁盘。
5.3 通知机制的设计与实现
审批流转过程中,如何让审批人知道"有一笔新申请需要处理"是这个系统体验的重要组成部分。我见过一些毕设完全没有通知功能,纯靠申请人电话口头催办,这个一定要避免。
推荐实现一套轻量级站内信通知。审批流转写入待办表的同时,往通知消息表插入一条记录,字段包括接收人ID、消息标题、消息内容、关联申请单ID、是否已读。用户登录后的首页侧边栏显示未读消息数量,点击消息列表跳转到对应申请单详情。这个功能看起来简单,但完整实现后,系统的闭环感立刻增强。
系统管理员的角色还可以在后台配置"审批超时提醒"。在通知表中存一个scheduled_time字段,用一个定时任务每天扫描哪些申请单在当前节点停留超过48小时,给节点审批人发催办站内信。机关单位环境下,"催办"功能往往比预期更受认可,因为领导们事多容易遗忘,这个功能解决的是真实的办公痛点。Spring Boot的@Scheduled注解做定时任务非常轻量,在启动类加上@EnableScheduling即可。
5.4 统计看板与数据导出
行政审批系统如果只有流程没有统计,方案展示时会显得单薄。我建议做一个首页统计看板,至少包含四张关键图表:各类审批事项的申请量占比、近七日的申请提交趋势、当前各节点的待办数量、审批通过率。统计SQL用MyBatis-Plus的selectMaps结合group by实现,返回的Map集合经过Service层组装成前端图表需要的JSON结构。前端建议接入ECharts,折线图和饼图的代码很成熟,域名匹配后展示效果非常加分。
数据导出功能也不能少。审批记录导出Excel是机关单位做台账的基本需求。用EasyExcel导出申请单列表和审批记录,注意导出时不要直接导出数据库的id和状态码,要转换成"科室初审""已通过"这类人类可读的字段,表格加一个编号列和审批日期列,就能直接当台账用。这个细节很多同学忽略,导出表里全是数字,演示效果大打折扣。
6. 部署实战与常见问题排查
6.1 本地开发到服务器部署的完整流程
这套系统从开发环境到可演示环境,我建议走"本地IDEA调试 + 服务器Docker部署"这条路径。本地开发时用application-dev.yml配置本地数据库,服务器上用application-prod.yml通过环境变量注入数据库地址和密码。
打包命令很简单:
mvn clean package -DskipTests打成jar包后放到服务器,用nohup java -jar approval-system.jar --spring.profiles.active=prod &启动。生产环境的数据库初始化用spring.sql.init.mode=always配合schema.sql和data.sql执行建表和初始数据脚本,注意第一次启动跑完后要把它改为never,避免每次重启都重新执行。
如果申请不到独立服务器,也可以直接在本地演示,关键是数据库和代码的环境配置要一致。很多同学在答辩前把项目放在自己电脑上,结果换了一台机器跑不起来,就是因为数据库地址写死了localhost、或者用了本机绝对路径存储附件。这里强烈建议把file.upload-dir这类配置放到配置文件中用相对路径,换环境时只需改一行配置。
6.2 常见异常与踩坑实录
这一节是全文最值钱的部分,全是我在开发和指导学生过程中真实碰到过的坑,按出现频率排列如下:
坑一:MyBatis-Plus的updateById不更新null字段。默认策略是FieldStrategy.NOT_NULL,字段为空就不生成到SET子句中。审批场景里清空某个字段再保存时,会发现旧值还在。解决方法是给实体字段加上@TableField(updateStrategy = FieldStrategy.IGNORED),或者用LambdaUpdateWrapper的set方法显式赋值。
坑二:Spring Security静态资源被拦截。引入了Bootstrap后登录页完全没样式,原因是没有放行静态资源路径。注意在SecurityConfig中antMatchers("/css/**", "/js/**", "/images/**", "/lib/**").permitAll(),否则登录页打开是纯HTML裸奔效果。
坑三:LocalDateTime在JSON返回时格式不对。前端显示"submitTime": "2024-05-20T14:30:00"这种带T的格式,原因是Jackson默认序列化LocalDateTime用的是ISO格式。在配置类里加一个Jackson2ObjectMapperBuilderCustomizer,设置LocalDateTime的序列化格式为yyyy-MM-dd HH:mm:ss,同时设置TimeZone为GMT+8,避免时区偏移八小时的问题。
坑四:事务方法内部被同类调用导致事务失效。Service中submitApplication调用同一个类里的generateNo方法,如果后者加了@Transactional,实际不会生效,因为Spring事务是基于代理实现的,同类内部调用不走代理。解决方式是把需要事务的代码放到不同Service类中互相调用,或者自己注入代理。
坑五:数据库表名user是MySQL保留字。建表时会直接报语法错误,创建用户表务必改成sys_user,这算是最老生常谈的坑,但是我发现每届都有人踩,这里再强调一次。
坑六:附件下载文件的ContentType设置错误。直接返回File对象时浏览器可能会乱码或预览异常,建议用ResponseEntity<Resource>配合Content-Disposition响应头指定文件名,同时文件名要做URL编码,否则中文文件名下载后会变成一堆乱码。
6.3 毕业设计答辩重点准备方向
代码能跑起来只是基础,答辩时的临场表现往往决定了最终成绩。我建议重点关注以下几个方向准备:
第一,业务流程完整性演示。从登录、提交申请、审批通过、流程办结,一路走完给老师看,突出每一步的状态变化和数据留痕。建议准备一组固定的演示账号(申请人、科室负责人、分管领导、主要负责人),演示时切换账号操作,让老师看到同一笔申请单在不同角色视图下的变化。
第二,技术亮点的主动展示。动态表单配置、待办推送表、乐观锁防并发审批这三个点,每一个都值得在演示时停下来讲两句,比如"这里我用JSON存储表单数据,所以新增审批事项不需要改表结构",比被动回答问题要加分太多。
第三,权限控制的现场验证。老师很喜欢在答辩时换个账号看能不能越权,建议大家事前就用普通用户登录试一下,确认普通用户访问审批管理接口会被Security拦截并跳转登录页。如果老师现场提出"能不能让A科室的人看到B科室的数据",这正好是把RBAC和数据权限分开设计的展示机会。
第四,规范性说明的准备。数据库设计说明、流程图、用例图要提前准备好,但不建议在PPT里堆大量的代码。用一张申请单状态流转图配合文字说明"每个节点由对应角色处理,处理后更新主表节点字段并记录审批日志",这个回答比念代码更让老师满意。
7. 系统扩展方向与个人经验分享
系统做完以后,不要急着结束项目,考虑几个扩展方向会对你的答辩和后续学习更有价值。
第一个可扩展方向是消息通知渠道的加法。目前站内信实现后,可以进一步接入邮件通知,当新申请到达节点时给审批人发送邮件摘要。Spring Boot整合JavaMailSender只需要配置邮箱服务器地址和账号,代码量不大,但把"系统主动触达用户"的体验完整化了。我之前做过一版,审批人只要看到邮件标题中的申请单编号,就知道需要处理哪一笔,实际使用下来催办效果比站内信好很多。
第二个方向是审批流程的可视化配置。当前流程节点是代码里写死的,如果要做更通用的产品,可以增加一张流程模板表,字段包括节点顺序、节点角色、通过去向、驳回去向,系统运行时读取模板执行流转。这一步是从"固定流程系统"迈向"可配置流程平台"的关键设计升级,做出来以后再接新的审批事项就不需要改任何流程代码了。
第三个方向是申请单数据的统计分析增强。教务或办公室部门通常有月度统计需求,比如每个月办了多张证明、平均每笔审批耗时几天。增加一个"审批时效统计"功能,计算每笔申请从提交到办结的耗时,按月份聚合出平均时长,这个功能在机关单位场景下非常实用,也是展示你对业务理解深度的好机会。
回到个人实际操作的体会。这个项目做完以后,我最大的感受是"审批类系统的价值不在于技术多花哨,而在于流程每一步是否可追溯、权限每一层是否可验证、数据每一笔是否可统计"。用SpringBoot做轻量级审批流转,让初学者可以专注于掌握Web开发的核心链路(认证、权限、CRUD、状态流转),同时又能接触到动态表单、乐观锁、数据权限这类有深度的工程问题,是一个适合在毕业设计中完成并讲清楚的项目。如果你正在为这类题目纠结,试着先把流程节点和数据库表关系想明白,代码部分其实水到渠成。最后再分享一个小技巧:演示环境里多造几笔状态不同的申请单,状态筛选器每个选项下都有数据可以点,这比临时造数据的演示观感好得多。