ARTICLE DETAIL

资讯详情

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

SSM办公用品管理系统:申领流程闭环与事务一致性实践

SSM办公用品管理系统:申领流程闭环与事务一致性实践 简介这是一套基于SSM框架SpringSpringMVCMyBatis开发的Java办公用品申领管理系统面向高校计算机专业学生、全栈初学者及课程设计/毕业设计实践者聚焦企业级办公物资全流程管理需求。系统采用角色驱动权限模型涵盖超级管理员、企业职工、审核员三类用户完整实现系统管理含用户、角色、菜单、个人信息与密码修改、库存物品维护、申领单据提交、多级审核通过/退回、审核记录查询及实时库存检索等核心业务功能。资源包共978个文件以309个JS脚本支撑jQuery EasyUI前端交互、264个PNG图标、168个CSS样式表含easyui.css、bootstrap.min.css等主流UI组件、44个Java后端类及18个JSP页面为主结构清晰、模块解耦适配JDK8Tomcat8.5MySQL5.7环境IDE推荐IntelliJ IDEA。目前已有127人学习下载代码无已知BUG前端采用AJAX异步通信界面美观实用可直接用于课程大作业或毕设原型开发亦适合动手实践SSM整合与权限控制逻辑。1. 这不是又一个“增删改查”DemoSSM办公用品管理系统的真实落地场景你可能已经见过几十个标着“SSM办公系统”的GitHub仓库点开一看全是用户管理部门管理角色管理的三件套连数据库字段都像复制粘贴出来的。但这个基于SSM的办公用品管理系统不一样——它把「申领流程」真正跑通了职工提交领用单→审核员在待审列表里点“通过”或“退回”→库存自动扣减/回滚→所有操作留痕可查。它不模拟审批流而是用真实事务控制MyBatis Spring Transaction保证“单据状态变更”和“库存数量更新”原子性。系统分三层权限超级管理员管全局配置企业职工只能填单查自己记录审核员专盯待审单据且看不到其他人的个人信息。前端用jQuery EasyUI而非Vue/React不是因为技术落后而是它原生支持datagrid分页、tabs嵌套、form验证一体化对JSP渲染友好部署到Tomcat后零构建步骤。适合想补全Java Web工程化能力的人——从mybatis-config.xml里typeAliases配置到SpringMVC中Valid校验注解绑定再到web.xml中CharacterEncodingFilter顺序每个环节都暴露在眼皮底下。2. SSM三层架构如何支撑办公用品申领闭环2.1 为什么选SSM而非Spring Boot——轻量级事务与JSP生态的刚性需求当前项目明确要求运行在Tomcat 8/9 JDK 8环境且前端深度依赖EasyUI的JSP Taglib如easyui:datagrid和jQuery插件链式调用。Spring Boot虽简化配置但其内嵌Tomcat与JSP兼容性存在已知限制尤其在JDK 8下需额外配置spring.mvc.view.prefix且无法直接使用% taglib prefixeasyui uri/WEB-INF/tld/easyui.tld%。而传统SSM组合中Spring MVC的InternalResourceViewResolver天然适配JSP路径映射MyBatis的SqlSessionFactoryBean可直接注入DataSource事务管理器DataSourceTransactionManager能精准控制Service层方法粒度。更重要的是办公用品申领涉及多表联动t_apply_form领用单、t_inventory库存、t_apply_record操作日志必须保证“审核通过时库存扣减单据状态更新日志写入”三者要么全成功要么全回滚。SSM中通过Transactional(rollbackFor Exception.class)标注在ApplyService.approveApply()方法上配合MyBatis的update语句批量执行比Spring Boot中需额外引入EnableTransactionManagement和PlatformTransactionManagerBean定义更直观。实际调试中发现若将事务注解放在Controller层会导致HTTP请求超时后事务未及时释放连接而放在Service实现类方法上Spring AOP代理能准确拦截并捕获SQLException触发回滚。2.2 数据库设计直击办公场景痛点从“物品静态属性”到“动态申领轨迹”MySQL 5.7建表脚本中t_inventory表并非简单存储“名称、数量、单价”而是包含min_stock_threshold最低库存阈值和is_alert_enabled是否启用预警字段。当某物品库存低于阈值时系统在首页顶部弹出红色预警条“【签字笔】库存仅剩3支低于安全线10支”。这种设计让库存管理从被动查询转向主动干预。t_apply_form表结构体现流程严谨性status字段采用枚举值0-待审核、1-已通过、2-已退回、3-已发放apply_time和approve_time分别记录申领时间与审核时间approver_id外键关联审核员ID而非用户名——避免姓名变更导致历史记录失效。最关键的t_apply_record日志表除常规operator_id、operation_type1-提交、2-审核通过、3-审核退回、4-库存扣减外还存有before_quantity和after_quantity字段。例如审核员点击“通过”时系统先查inventory.quantity再执行UPDATE t_inventory SET quantity quantity - #{applyCount} WHERE id #{itemId}最后插入日志记录变更前后的具体数值。这种设计使审计人员能直接追溯“签字笔库存为何从15支变为12支”无需拼接多条SQL日志。-- 示例审核通过时的库存扣减SQLMyBatis Mapper XML update iddecreaseInventory parameterTypemap UPDATE t_inventory SET quantity quantity - #{applyCount}, update_time NOW() WHERE id #{itemId} AND quantity #{applyCount} -- 防止超领 /update提示该SQL中AND quantity #{applyCount}是硬性校验若库存不足则UPDATE影响行数为0后续Service层会抛出自定义异常InsufficientStockException前端EasyUI弹窗提示“库存不足请联系管理员”。2.3 EasyUI布局如何规避JSP页面臃肿——模块化Tab与动态加载策略项目前端未采用单页应用SPA模式而是利用EasyUI的div classeasyui-tabs实现功能区隔离。主页面index.jsp只包含导航菜单和Tabs容器各功能模块如库存管理、单据审核作为独立JSP文件inventory.jsp、audit.jsp通过href属性异步加载!-- index.jsp 中的Tabs定义 -- div classeasyui-tabs>bean idshiroFilter classorg.apache.shiro.spring.web.ShiroFilterFactoryBean property namesecurityManager refsecurityManager/ property nameloginUrl value/login.jsp/ property nameunauthorizedUrl value/unauthorized.jsp/ property namefilterChainDefinitionMap refchainDefinitionMap/ /bean !-- URL权限映射 -- bean idchainDefinitionMap classjava.util.LinkedHashMap constructor-arg map entry key/login.action valueanon/ entry key/logout.action valuelogout/ entry key/admin/** valueauthc,roles[admin]/ entry key/user/** valueauthc,perms[user:apply]/ entry key/audit/** valueauthc,perms[audit:approve]/ entry key/** valueauthc/ /map /constructor-arg /bean此处/admin/**路径需admin角色/audit/**需audit:approve权限。权限字符串与数据库sys_role_permission表中的permission_code字段严格对应。当用户登录后Shiro会调用自定义RealmCustomRealm.java查询其角色及权限集合缓存至SecurityUtils.getSubject().getPrincipals()。前端EasyUI菜单栏通过AJAX请求/menu/list接口获取JSON格式菜单树Controller层根据当前用户权限动态过滤// MenuController.java RequestMapping(/menu/list) ResponseBody public ListMenu getMenuList() { Subject subject SecurityUtils.getSubject(); ListString permissions (ListString) subject.getPrincipal().getPermissions(); return menuService.getMenuByPermissions(permissions); // 仅返回用户有权访问的菜单 }3.2 菜单动态渲染的边界处理禁用项、隐藏项与权限继承EasyUI菜单生成时需处理三种状态完全不可见如审核员看不到“用户管理”菜单、可见但禁用如普通职工能看到“单据审核”菜单项但点击无响应、可见且可用。项目通过Menu实体类的isHidden和isDisabled字段控制public class Menu { private Long id; private String name; private String url; private Integer sort; private Boolean isHidden false; // true则前端不渲染 private Boolean isDisabled false; // true则菜单项加disabled class private String permissionCode; // 权限码用于Shiro校验 }特别注意权限继承机制超级管理员拥有*:*通配权限但系统未在数据库中存储该字符串而是在CustomRealm.doGetAuthorizationInfo()中硬编码if (admin.equals(roleName)) { authorizationInfo.addStringPermission(*:*); // 赋予全部权限 } else { // 查询角色对应的具体权限码 ListString perms permissionMapper.findByRoleId(roleId); authorizationInfo.addStringPermissions(perms); }这种设计避免数据库中出现模糊权限同时保证超级管理员能访问所有URL包括未在菜单表中配置的调试接口。3.3 前端按钮级权限控制EasyUI DataGrid操作列的条件渲染DataGrid中“编辑”、“删除”按钮不能仅靠后端拦截必须前端隐藏以提升体验。项目在audit.jsp中使用EasyUI的formatter函数动态生成操作列{field:action,title:操作,width:120,align:center, formatter:function(value,row,index){ var btns ; if(hasPermission(apply:audit:pass)){ // 前端权限校验 btns a hrefjavascript:void(0) onclickpassApply(row.id)通过/a | ; } if(hasPermission(apply:audit:return)){ btns a hrefjavascript:void(0) onclickreturnApply(row.id)退回/a; } return btns; }}hasPermission()函数从全局变量userPermissions登录后由/user/permissions接口注入中查找function hasPermission(code) { return userPermissions.indexOf(code) -1; }注意此校验仅为UI层友好后端Controller仍需用RequiresPermissions(apply:audit:pass)二次校验防止恶意用户绕过前端直接调用API。4. 领用单据审核的事务一致性保障与并发控制4.1 库存扣减的乐观锁实现避免超卖与幻读当多个审核员同时处理同一物品的领用单时可能出现库存超扣问题。项目未采用悲观锁SELECT ... FOR UPDATE而是用乐观锁版本号机制。t_inventory表增加version字段初始值0每次更新库存时校验版本update iddecreaseInventoryWithVersion parameterTypemap UPDATE t_inventory SET quantity quantity - #{applyCount}, version version 1, update_time NOW() WHERE id #{itemId} AND version #{version} AND quantity #{applyCount} /updateService层调用时public void approveApply(Long applyId) { ApplyForm form applyMapper.selectById(applyId); Inventory inventory inventoryMapper.selectById(form.getItemId()); int updated inventoryMapper.decreaseInventoryWithVersion( Map.of(itemId, form.getItemId(), applyCount, form.getCount(), version, inventory.getVersion()) ); if (updated 0) { throw new RuntimeException(库存更新失败可能已被其他审核员处理); } // 更新单据状态、写入日志... }此方案优势在于不阻塞其他审核员操作不同物品且数据库层面保证原子性。若updated0说明WHERE version ?条件不匹配即库存记录已被更新此时抛出异常并提示用户“请刷新页面重试”。4.2 审核日志的完整链路追踪从HTTP请求到数据库事务每条审核操作均生成三条日志HTTP层LogInterceptor记录请求URL、参数、耗时、响应状态码Service层Transactional开启事务时TransactionSynchronizationManager.getCurrentTransactionName()获取事务名如ApplyService.approveApplyDAO层MyBatisExecutor拦截器捕获SQL执行详情含参数值、影响行数。最终日志聚合至t_apply_record表字段trace_id关联同一事务INSERT INTO t_apply_record ( apply_id, operator_id, operation_type, before_quantity, after_quantity, trace_id, create_time ) VALUES ( #{applyId}, #{userId}, #{operationType}, #{beforeQuantity}, #{afterQuantity}, #{traceId}, NOW() );traceId由UUID生成贯穿Controller→Service→Mapper全链路。当出现“单据状态变为已通过但库存未扣减”时可直接查trace_id定位具体哪一步骤失败。4.3 AJAX交互中的错误处理EasyUI Form提交的防抖与重试前端EasyUI Form提交领用单时为防止用户重复点击导致多条单据采用双重防护$(#applyForm).form(submit, { url: apply/submit.action, onSubmit: function(){ if($(this).form(validate)) { $(this).closest(form).find(:submit).attr(disabled,disabled).text(提交中...); return true; } return false; }, success: function(data){ var result JSON.parse(data); if(result.success){ $.messager.alert(成功,申领单提交成功); $(#applyForm).form(clear); } else { $.messager.alert(错误, result.msg || 提交失败); } // 恢复按钮状态 $(this).closest(form).find(:submit).removeAttr(disabled).text(提交); } });提示onSubmit中return true才真正发起AJAXsuccess回调中必须手动恢复按钮状态否则网络延迟时用户可能误以为提交失败而再次点击。5. 开发调试与部署避坑指南从IDEA配置到Tomcat优化5.1 IntelliJ IDEA中SSM项目的关键配置项在IDEA中导入项目后必须检查以下三项配置否则JSP无法解析或静态资源404配置项路径正确值说明Project SDKFile → Project Structure → ProjectJDK 1.8若选JDK 11javax.servlet包会报错ArtifactsProject Structure → Artifactsexploded类型包含WEB-INF/lib/*.jar和/pages目录缺少/pages则EasyUI子页面加载失败Tomcat ServerRun → Edit Configurations → Tomcat ServerDeployment中选择Artifact: xxx:war exploded必须选exploded而非war否则JSP修改需重启特别注意web.xml中welcome-file-list需指定index.jsp且servlet-mapping的url-pattern为/而非*.action否则EasyUI的href异步加载会404。5.2 MySQL 5.7字符集与JDBC连接参数调优项目使用mysql-connector-java 5.1.47驱动jdbc.properties中必须显式声明字符集jdbc.urljdbc:mysql://localhost:3306/office_system?useUnicodetruecharacterEncodingUTF-8serverTimezoneGMT%2B8useSSLfalse若遗漏serverTimezoneGMT%2B8JDBC会抛出java.sql.SQLException: The server time zone value XXX is unrecognized。此外my.cnf中需设置[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci否则中文用户名或物品名称在LIKE查询时可能乱码。5.3 生产环境Tomcat内存与线程池配置建议针对办公系统并发量不高通常100人在线的特点conf/server.xml中调整ConnectorConnector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 maxThreads200 !-- 默认200足够 -- minSpareThreads20 maxSpareThreads75 acceptCount100 !-- 队列长度 -- URIEncodingUTF-8/同时在bin/catalina.sh中增加JVM参数JAVA_OPTS-Xms512m -Xmx1024m -XX:MetaspaceSize128m -XX:MaxMetaspaceSize256m避免因Metaspace溢出导致频繁Full GC。经实测该配置下Tomcat 8.5可稳定运行3个月无内存泄漏。5.4 一个实用技巧用Chrome开发者工具快速定位EasyUI样式冲突当EasyUI组件如DateBox在IE8下日期选择器错位或Bootstrap按钮被EasyUI CSS覆盖时按F12打开DevTools执行以下步骤在Elements面板中右键目标元素 →Break on → attribute modifications刷新页面当EasyUI JS动态添加classdatebox时断点触发查看Call Stack定位到jquery.easyui.min.js第12345行具体行号因版本而异在Console中输入$.fn.datebox.defaults.panelHeight 200临时修复高度。此方法比盲目修改CSS更高效且能确认问题根源是JS计算还是CSS优先级。本文还有配套的精品资源点击获取
返回列表