十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

轻量级财务审批流系统:基于SSM的状态机实现

轻量级财务审批流系统:基于SSM的状态机实现 简介这是一套基于Java SSM框架实现的轻量级财务流程管理系统聚焦发票管理与三级审批流员工→负责人→总经理业务场景适合Java Web初学者及中小型项目开发者学习审批流程设计与权限控制实践。资源包共399个文件涵盖65个JSP页面、43个Java后端类、76个XML配置文件、46个JS脚本及21个CSS样式文件支撑MVC分层结构与Layui前端交互另有SQL建表脚本、数据库初始化数据及多格式图标资源整体压缩包仅1.74MB便于快速导入与本地调试。已有995人学习下载提供完整可运行环境JDK8Tomcat8MySQL含三角色账号预置逻辑与清晰权限划分登录地址为http://localhost:8080/login配套staff表说明便于理解权限映射关系是掌握SSM整合开发与工作流落地的典型教学案例。1. 一个跑在 Tomcat 8 上的轻量级财务审批流系统它不是 BPM但能解决发票报销卡点问题你不需要部署 Activiti 或 Flowable也能让财务流程跑起来。这个基于 SSMSpring Spring MVC MyBatis搭建的财务流程管理系统核心只做一件事把「员工提交发票 → 负责人初审 → 总经理终审」这条链路用数据库状态驱动前端按钮控制闭环落地。它不抽象流程定义、不支持并行网关、不生成 BPMN 图但恰恰因此——启动快JDK8Tomcat8 即开即用、改得快MyBatis XML SQL 直接调、看得清所有审批记录存approval_record表含操作人、时间、意见、当前状态。适合中小团队快速上线发票类单据的最小可行审批流尤其适配已有 MySQL 基础设施、开发人力集中在 Java 后端、前端只需 LayUI 快速搭界面的场景。如果你正被「报销单总在负责人邮箱里沉睡」困扰又不想引入复杂工作流引擎这个项目就是可直接编译、改表字段、加审批节点的实操样本。2. 审批流状态机设计与 SSM 分层实现逻辑2.1 为什么用「状态码 角色权限」代替流程引擎选型依据与边界主流审批系统常陷入两个极端一类是硬编码 if-else 判断角色和状态易维护但难扩展另一类是引入 Activiti 等引擎可配置但学习成本高、部署重。本项目采用折中方案——数据库驱动的状态机在invoice表中设status字段tinyint取值为0草稿, 1待负责人审核, 2待总经理审核, 3已通过, 4已驳回再结合staff.role字段varchar值为employee/manager/director控制按钮可见性与操作权限。这种设计规避了流程引擎的依赖同时避免了纯硬编码导致的状态分支爆炸。关键在于状态流转不靠代码分支判断而靠 SQL UPDATE 的 WHERE 条件约束。例如只有status1 AND current_rolemanager才允许执行「同意」操作否则 UPDATE 影响行为 0。这使得业务规则全部沉淀在 DAO 层 SQL 中Controller 只做参数校验与跳转符合 SSM 分层清晰、各司其职的设计哲学。提示该方案的适用边界明确——流程节点固定仅 3 级、无退回/加签/会签等复杂分支、审批人角色与组织架构强绑定。若需支持动态路由或跨部门协同应评估引入轻量级引擎如 JFlow 或自研规则引擎。2.2 数据库表结构与关键字段语义解析系统核心表共 4 张均位于src/main/resources/sql/下的初始化脚本中。重点看invoice和approval_record-- 发票主表含审批状态 CREATE TABLE invoice ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(100) DEFAULT NULL COMMENT 发票标题, amount decimal(10,2) DEFAULT NULL COMMENT 金额, submitter_id bigint(20) DEFAULT NULL COMMENT 提交人ID关联staff, status tinyint(4) DEFAULT 0 COMMENT 状态0草稿/1待负责人审/2待总经理审/3通过/4驳回, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 审批记录表留痕关键 CREATE TABLE approval_record ( id bigint(20) NOT NULL AUTO_INCREMENT, invoice_id bigint(20) NOT NULL COMMENT 关联发票ID, approver_id bigint(20) NOT NULL COMMENT 审批人ID, opinion text COMMENT 审批意见, status_after tinyint(4) NOT NULL COMMENT 审批后状态同invoice.status, operate_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_invoice_id (invoice_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status字段是状态机中枢所有业务逻辑围绕其值展开。approval_record不仅记录结果更承担「谁在什么时间把单据从状态 A 推到状态 B」的审计职责。注意status_after与invoice.status保持一致但前者不可更新——这是防止篡改审批历史的关键设计。2.3 SSM 各层关键代码实现与参数说明2.3.1 MyBatis Mapper 接口与 XML 实现状态安全更新InvoiceMapper.java定义核心方法// InvoiceMapper.java public interface InvoiceMapper { // 根据ID查询发票带状态 Invoice selectById(Long id); // 安全更新状态仅当原状态匹配且当前用户有权限时才生效 int updateStatusByCondition(Param(id) Long id, Param(oldStatus) Byte oldStatus, Param(newStatus) Byte newStatus, Param(currentRoleId) String currentRoleId); }对应InvoiceMapper.xml中的 SQL!-- InvoiceMapper.xml -- update idupdateStatusByCondition UPDATE invoice SET status #{newStatus}, update_time NOW() WHERE id #{id} AND status #{oldStatus} AND ( (#{oldStatus} 0 AND #{currentRoleId} employee) -- 员工可提交草稿 OR (#{oldStatus} 1 AND #{currentRoleId} manager) -- 负责人可审第一级 OR (#{oldStatus} 2 AND #{currentRoleId} director) -- 总经理可审第二级 ) /update这段 SQL 是状态机的执行核心。WHERE子句同时校验原状态和当前角色权限确保只有合法角色才能触发对应状态变更。例如当oldStatus1且currentRoleIdmanager时UPDATE 才会生效若currentRoleIdemployee即使传入oldStatus1SQL 也找不到匹配行updateStatusByCondition()返回 0Controller 便可据此返回「无权操作」提示。2.3.2 Spring MVC Controller 的审批动作处理逻辑InvoiceController.java中的审批接口// InvoiceController.java PostMapping(/invoice/approve) ResponseBody public ResultString approve(RequestParam Long invoiceId, RequestParam Byte newStatus, RequestParam String opinion, HttpServletRequest request) { Staff staff (Staff) request.getSession().getAttribute(loginUser); if (staff null) return Result.fail(未登录); // 1. 查询当前发票状态 Invoice invoice invoiceService.selectById(invoiceId); if (invoice null) return Result.fail(发票不存在); // 2. 根据当前状态推导期望的 oldStatus 和角色要求 Byte oldStatus; String requiredRole; switch (invoice.getStatus()) { case 0: return Result.fail(草稿状态不可审批); case 1: oldStatus 1; requiredRole manager; break; case 2: oldStatus 2; requiredRole director; break; default: return Result.fail(当前状态不可审批); } // 3. 执行安全更新 int rows invoiceService.updateStatusByCondition(invoiceId, oldStatus, newStatus, requiredRole); if (rows 0) { return Result.fail(审批失败无权操作或状态已变更); } // 4. 记录审批日志 ApprovalRecord record new ApprovalRecord(); record.setInvoiceId(invoiceId); record.setApproverId(staff.getId()); record.setOpinion(opinion); record.setStatusAfter(newStatus); approvalRecordService.insert(record); return Result.success(审批成功); }此 Controller 严格遵循「查→判→更→记」四步法。关键点在于状态校验与角色校验分离——先由业务逻辑判断requiredRole再交由 MyBatis SQL 的WHERE子句二次验证形成双重保险。Result封装统一响应格式前端可据此刷新页面或弹窗提示。3. LayUI 前端审批界面与权限按钮控制实战3.1 登录态与角色信息注入机制系统未使用 Spring Security而是通过LoginController手动管理 Session// LoginController.java PostMapping(/login) public String login(RequestParam String username, RequestParam String password, HttpServletRequest request, Model model) { Staff staff staffService.login(username, password); if (staff ! null) { request.getSession().setAttribute(loginUser, staff); // 将角色写入 request scope供 Thymeleaf 模板读取 request.setAttribute(role, staff.getRole()); return redirect:/invoice/list; // 根据角色跳转不同首页 } else { model.addAttribute(error, 用户名或密码错误); return login; } }LayUI 页面如invoice_list.html通过 Thymeleaf 获取角色并控制按钮!-- invoice_list.html -- div classlayui-btn-group th:if${role manager} button classlayui-btn layui-btn-sm onclickapprove(this, [[${invoice.id}]], 2)同意/button button classlayui-btn layui-btn-danger layui-btn-sm onclickreject(this, [[${invoice.id}]], 4)驳回/button /div div classlayui-btn-group th:if${role director} th:unless${invoice.status 1} !-- 总经理只对 status2 的单据显示按钮 -- button classlayui-btn layui-btn-sm onclickapprove(this, [[${invoice.id}]], 3)批准/button button classlayui-btn layui-btn-danger layui-btn-sm onclickreject(this, [[${invoice.id}]], 4)否决/button /divth:if和th:unless在服务端渲染时即完成权限过滤避免前端 JS 拼接按钮后又被后端拦截提升用户体验与安全性。3.2 审批弹窗与 AJAX 提交完整流程点击「同意」按钮触发 LayUI 弹窗收集意见function approve(btn, invoiceId, newStatus) { layer.prompt({title: 请输入审批意见, formType: 2}, function(value, index) { $.post(/invoice/approve, { invoiceId: invoiceId, newStatus: newStatus, opinion: value }, function(res) { if (res.code 0) { layer.msg(审批成功, {icon: 1}); layer.close(index); // 刷新当前表格使用 LayUI table.reload table.reload(invoiceTable, {page: {curr: 1}}); } else { layer.msg(res.msg, {icon: 2}); } }); }); }此处table.reload是 LayUI Table 模块的核心方法它重新请求/invoice/list接口并渲染表格确保状态变更后列表实时更新。注意/invoice/list接口在后端会根据loginUser.role动态拼接 SQL 查询条件如负责人只查status1的单据实现数据级权限隔离。3.3 关键 CSS 文件作用与定制要点项目包含多个 CSS 文件分工明确文件名作用定制建议layui.cssLayUI 组件基础样式按钮、表单、弹窗勿修改升级 LayUI 时直接替换main.css全局布局、导航栏、页脚样式可调整.header-logo字体大小、.sidebar背景色login.css登录页专属样式居中卡片、输入框阴影修改.login-box的max-width适配大屏image.css头像、附件预览相关样式若需支持 PDF 预览可在此添加.pdf-icon类特别注意icon.css——它定义了 LayUI 图标字体映射如i classlayui-icon#xe60c;/i对应「通过」图标。若需新增图标需在icon.css中添加 Unicode 编码映射并确保字体文件iconfont.woff存在。4. 本地运行与三角色账号调试全流程4.1 环境准备与 Maven 依赖验证确保 JDK 8、Tomcat 8、MySQL 5.7 已就绪。检查pom.xml中 SSM 核心依赖版本properties spring.version5.2.20.RELEASE/spring.version mybatis.version3.4.6/mybatis.version mysql.connector.version5.1.47/mysql.connector.version /properties dependencies !-- Spring Core -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency !-- MyBatis-Spring 整合 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version1.3.2/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version${mysql.connector.version}/version /dependency /dependencies注意mysql-connector-java版本必须与 MySQL 服务端兼容。若使用 MySQL 8.0需升级至8.0.28并在 JDBC URL 中添加serverTimezoneGMT%2B8参数否则连接时抛出java.sql.SQLException: The server time zone value ... is unrecognized。4.2 数据库初始化与账号查看执行src/main/resources/sql/init.sql创建库表并插入测试数据-- 初始化 staff 表三个角色账号 INSERT INTO staff (id, username, password, name, role) VALUES (1, user1, e10adc3949ba59abbe56e057f20f883e, 张三, employee), (2, user2, e10adc3949ba59abbe56e057f20f883e, 李四, manager), (3, user3, e10adc3949ba59abbe56e057f20f883e, 王五, director);密码均为123456的 MD5 值e10adc3949ba59abbe56e057f20f883e。登录后可在staff表中直接修改密码字段新密码需 MD5 加密或新增账号。4.3 IntelliJ IDEA 配置与启动调试导入项目File → Open → 选择项目根目录 → 选中pom.xml→ 点击 OK配置 TomcatRun → Edit Configurations → → Tomcat Server → Local → Deployment → → Artifact → 选择financial-flow:war exploded设置 VM Options避免中文乱码-Dfile.encodingUTF-8 -Dsun.jnu.encodingUTF-8配置 Application Properties在src/main/resources/application.properties中确认数据库连接jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/financial_flow?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8 jdbc.usernameroot jdbc.passwordyour_password启动后访问http://localhost:8080/login依次用user1/user2/user3登录观察不同角色看到的发票列表与按钮差异。4.4 常见启动报错与定位方法报错现象原因定位命令/步骤java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServletSpring MVC 依赖缺失检查pom.xml是否含spring-webmvc执行mvn dependency:tree | grep spring-webmvcFailed to bind properties under jdbcapplication.properties格式错误检查等号前后是否有空格用vim -u NONE查看隐藏字符Access denied for user rootlocalhostMySQL 密码错误或权限不足在 MySQL 中执行SELECT User,Host FROM mysql.user;确认 root 用户存在再ALTER USER rootlocalhost IDENTIFIED BY your_password;页面空白F12 显示404 /js/layui.js静态资源路径错误检查webapp/目录下是否存在js/layui.js确认web.xml中welcome-file-list是否指向index.jsp5. 审批流扩展技巧从三节点到多级会签的平滑演进5.1 新增审批节点的最小改动方案假设需增加「财务部复核」环节第 3 级只需三处修改扩展invoice.status枚举在Invoice.java中增加STATUS_WAIT_FINANCE 3;并更新数据库invoice.status字段注释。修改updateStatusByConditionSQL在InvoiceMapper.xml的WHERE子句中追加OR (#{oldStatus} 2 AND #{currentRoleId} finance) -- 新增财务角色新增角色权限控制在staff.role字段插入finance值并在invoice_list.html中添加对应按钮div classlayui-btn-group th:if${role finance} th:unless${invoice.status ! 2} button classlayui-btn onclickapprove(this, [[${invoice.id}]], 4)复核通过/button /div此方案无需改动 Controller 层状态机逻辑由 SQL 自动承载符合「小步快跑」原则。5.2 审批意见富文本支持改造当前opinion字段为text类型仅支持纯文本。若需支持加粗、列表、图片可集成 UMEditor引入 UMEditor JS/CSS将umeditor.min.js、umeditor.config.js、umeditor.css放入webapp/js/和webapp/css/修改审批弹窗 HTMLscript typetext/plain idcontainer nameopinion/script script src/js/umeditor.min.js/script script var um UM.getEditor(container); // 提交时获取 HTML 内容 var opinionHtml um.getContent(); /script后端接收改为String类型ApprovalRecord.opinion字段类型不变但存储 HTML 字符串前端展示时用v-html渲染若用 Vue或innerHTML原生 JS注意存储 HTML 需防范 XSS应在保存前用Jsoup.clean(opinionHtml, Whitelist.relaxed())过滤危险标签。5.3 审批超时自动提醒配置表设计为防审批滞留可增加approval_timeout表记录超时规则CREATE TABLE approval_timeout ( id bigint(20) NOT NULL AUTO_INCREMENT, status_from tinyint(4) NOT NULL COMMENT 从哪个状态开始计时, hours int(11) NOT NULL COMMENT 超时小时数, notify_role varchar(20) NOT NULL COMMENT 超时通知角色manager/director, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 示例状态1待负责人审超时24小时通知负责人 INSERT INTO approval_timeout VALUES (1, 1, 24, manager);配合定时任务如Scheduled(fixedRate 300000)每5分钟扫描查询invoice中status1 AND create_time DATE_SUB(NOW(), INTERVAL 24 HOUR)的记录调用邮件或站内信接口推送提醒。此设计将业务规则外置为配置避免硬编码超时逻辑。实际部署时可将Scheduled替换为 Quartz 集群调度确保多实例环境下仅一个节点执行扫描任务。本文还有配套的精品资源点击获取
返回列表