简介:这是一套基于SSM框架(Spring+SpringMVC+MyBatis)开发的Java办公用品申领管理系统,面向高校计算机专业学生、全栈初学者及课程设计/毕业设计实践者,聚焦企业级办公物资全流程管理需求。系统采用角色驱动权限模型,涵盖超级管理员、企业职工、审核员三类用户,完整实现系统管理(含用户、角色、菜单、个人信息与密码修改)、库存物品维护、申领单据提交、多级审核(通过/退回)、审核记录查询及实时库存检索等核心业务功能。资源包共978个文件,以309个JS脚本(支撑jQuery EasyUI前端交互)、264个PNG图标、168个CSS样式表(含easyui.css、bootstrap.min.css等主流UI组件)、44个Java后端类及18个JSP页面为主,结构清晰、模块解耦,适配JDK8+Tomcat8.5+MySQL5.7环境,IDE推荐IntelliJ IDEA。目前已有127人学习下载,代码无已知BUG,前端采用AJAX异步通信,界面美观实用,可直接用于课程大作业或毕设原型开发,亦适合动手实践SSM整合与权限控制逻辑。
1. 这不是又一个“增删改查”Demo:SSM办公用品管理系统的真实落地场景
你可能已经见过几十个标着“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 prefix="easyui" 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_type(1-提交、2-审核通过、3-审核退回、4-库存扣减)外,还存有before_quantity和after_quantity字段。例如审核员点击“通过”时,系统先查inventory.quantity,再执行UPDATE t_inventory SET quantity = quantity - #{applyCount} WHERE id = #{itemId},最后插入日志记录变更前后的具体数值。这种设计使审计人员能直接追溯“签字笔库存为何从15支变为12支”,无需拼接多条SQL日志。
-- 示例:审核通过时的库存扣减SQL(MyBatis Mapper XML) <update id="decreaseInventory" parameterType="map"> 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 class="easyui-tabs">实现功能区隔离。主页面index.jsp只包含导航菜单和Tabs容器,各功能模块(如库存管理、单据审核)作为独立JSP文件(inventory.jsp、audit.jsp)通过href属性异步加载:
<!-- index.jsp 中的Tabs定义 --> <div class="easyui-tabs"><bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitionMap" ref="chainDefinitionMap"/> </bean> <!-- URL权限映射 --> <bean id="chainDefinitionMap" class="java.util.LinkedHashMap"> <constructor-arg> <map> <entry key="/login.action" value="anon"/> <entry key="/logout.action" value="logout"/> <entry key="/admin/**" value="authc,roles[admin]"/> <entry key="/user/**" value="authc,perms[user:apply]"/> <entry key="/audit/**" value="authc,perms[audit:approve]"/> <entry key="/**" value="authc"/> </map> </constructor-arg> </bean>此处/admin/**路径需admin角色,/audit/**需audit:approve权限。权限字符串与数据库sys_role_permission表中的permission_code字段严格对应。当用户登录后,Shiro会调用自定义Realm(CustomRealm.java)查询其角色及权限集合,缓存至SecurityUtils.getSubject().getPrincipals()。前端EasyUI菜单栏通过AJAX请求/menu/list接口获取JSON格式菜单树,Controller层根据当前用户权限动态过滤:
// MenuController.java @RequestMapping("/menu/list") @ResponseBody public List<Menu> getMenuList() { Subject subject = SecurityUtils.getSubject(); List<String> permissions = (List<String>) 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 { // 查询角色对应的具体权限码 List<String> 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 href="javascript:void(0)" onclick="passApply('+row.id+')">通过</a> | '; } if(hasPermission('apply:audit:return')){ btns += '<a href="javascript:void(0)" onclick="returnApply('+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 id="decreaseInventoryWithVersion" parameterType="map"> UPDATE t_inventory SET quantity = quantity - #{applyCount}, version = version + 1, update_time = NOW() WHERE id = #{itemId} AND version = #{version} AND quantity >= #{applyCount} </update>Service层调用时:
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("库存更新失败:可能已被其他审核员处理"); } // 更新单据状态、写入日志... }此方案优势在于:不阻塞其他审核员操作不同物品,且数据库层面保证原子性。若updated==0,说明WHERE version = ?条件不匹配,即库存记录已被更新,此时抛出异常并提示用户“请刷新页面重试”。
4.2 审核日志的完整链路追踪:从HTTP请求到数据库事务
每条审核操作均生成三条日志:
- HTTP层:
LogInterceptor记录请求URL、参数、耗时、响应状态码; - Service层:
@Transactional开启事务时,TransactionSynchronizationManager.getCurrentTransactionName()获取事务名(如ApplyService.approveApply); - DAO层:MyBatis
Executor拦截器捕获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才真正发起AJAX,success回调中必须手动恢复按钮状态,否则网络延迟时用户可能误以为提交失败而再次点击。
5. 开发调试与部署避坑指南:从IDEA配置到Tomcat优化
5.1 IntelliJ IDEA中SSM项目的关键配置项
在IDEA中导入项目后,必须检查以下三项配置,否则JSP无法解析或静态资源404:
| 配置项 | 路径 | 正确值 | 说明 |
|---|---|---|---|
| Project SDK | File → Project Structure → Project | JDK 1.8 | 若选JDK 11+,javax.servlet包会报错 |
| Artifacts | Project Structure → Artifacts | exploded类型,包含WEB-INF/lib/*.jar和/pages目录 | 缺少/pages则EasyUI子页面加载失败 |
| Tomcat Server | Run → Edit Configurations → Tomcat Server | Deployment中选择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.url=jdbc:mysql://localhost:3306/office_system?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false若遗漏serverTimezone=GMT%2B8,JDBC会抛出java.sql.SQLException: The server time zone value 'XXX' is unrecognized。此外,my.cnf中需设置:
[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci否则中文用户名或物品名称在LIKE查询时可能乱码。
5.3 生产环境Tomcat内存与线程池配置建议
针对办公系统并发量不高(通常<100人在线)的特点,conf/server.xml中调整Connector:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxThreads="200" <!-- 默认200足够 --> minSpareThreads="20" maxSpareThreads="75" acceptCount="100" <!-- 队列长度 --> URIEncoding="UTF-8"/>同时,在bin/catalina.sh中增加JVM参数:
JAVA_OPTS="-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m"避免因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动态添加
class="datebox"时断点触发; - 查看Call Stack,定位到
jquery.easyui.min.js第12345行(具体行号因版本而异); - 在Console中输入
$.fn.datebox.defaults.panelHeight = 200临时修复高度。
此方法比盲目修改CSS更高效,且能确认问题根源是JS计算还是CSS优先级。
本文还有配套的精品资源,点击获取