ARTICLE DETAIL

资讯详情

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

SSM整合实战:企业绩效考核管理系统设计与实现

SSM整合实战:企业绩效考核管理系统设计与实现 1. 从需求梳理到技术选型为什么这套组合到今天依然能打先说个有意思的现象。这两年 Spring Boot 和 Vue 几乎成了新项目的默认选择但你去企业里翻一翻尤其是传统制造业、国企、银行外包、高校实验室大量存量的 JavaWeb 项目还是 SSM JSP 架构在跑。绩效考核这类企业内部管理系统正是这些老系统里最常见的一类。我这次做的这套企业绩效考核管理系统技术栈就是标题里写的Java SSM Maven JSP Bootstrap jQuery MySQL一个都没少。有人会问现在都 2024 年了为什么还要用 SSM 手写 JSP答案很简单第一很多企业已经投产的 OA、ERP 系统就是这套架构新开发的功能必须和老系统保持技术栈一致否则后面没人维护第二绩效考核系统的核心是复杂的业务逻辑和报表统计前端交互并不需要多花哨JSP 服务端渲染反而更稳Bootstrap jQuery 足够覆盖所有操作界面第三对很多刚入行或者做课程设计、毕业设计的同学来说这套组合正好能把 Servlet、Spring 容器、MyBatis 映射、Maven 依赖管理这些基础功全部练到是性价比最高的练手项目。回到项目本身。这套绩效考核系统的业务目标很清晰企业里每个员工都需要在月初定目标、月末被打分部门主管要对下属的工作业绩、工作能力、工作态度逐项评分HR 需要能看到全公司的考核结果并导出汇总表。围绕这个目标我把它拆成了四个核心模块系统登录与权限控制普通员工、部门主管、系统管理员三类角色登录后看到不同的菜单和操作按钮。考核指标维护管理员维护考核维度业绩、能力、态度和每项指标的权重。考核打分流程主管对下属按指标逐项打分提交后生成考核记录员工能查看自己的得分。统计报表按部门、按时间周期汇总每个人的总分和排名支持导出。如果说这套系统放到 Spring Boot 里可能很多配置都是自动化完成的但 SSM 架构下每一步都是手写的这反而让我把整个请求流转过程过了一遍。这篇文章我不会只贴完整项目代码——那对你的帮助有限——而是把这套系统从 0 到 1 的关键设计与实现细节全部拆开讲清楚。数据结构怎么设计、SSM 三个框架怎么整合最省事、Maven pom 怎么配才不会引入冲突、JSP jQuery 怎么做异步交互、最后怎么打包部署每一块我都会展开说。2. 数据库设计五张核心表撑起一套考核流程绩效考核系统的业务核心在于人、指标、分数、周期这四个要素的联动。我在设计表结构时没有过度设计一共五张表用户表、部门表、考核指标表、考核模板表、考核记录表。下面逐一说明设计思路。2.1 用户表和部门表角色与组织架构的载体用户表 sys_user 是最基础的表字段包括用户ID、登录账号、密码MD5加密存储、用户姓名、所属部门ID、角色1-管理员2-主管3-员工、直属上级ID、状态0-禁用1-启用。CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT 密码MD5, real_name VARCHAR(50) NOT NULL COMMENT 真实姓名, dept_id INT COMMENT 所属部门ID, role INT NOT NULL DEFAULT 3 COMMENT 角色 1管理员 2主管 3员工, leader_id INT COMMENT 直属上级ID, status INT NOT NULL DEFAULT 1 COMMENT 状态 0禁用 1启用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );直属上级这个字段很关键它直接决定了考核打分时的数据范围。主管登录后查我的下属就是SELECT * FROM sys_user WHERE leader_id 当前登录用户ID。这个设计比单独建一张用户-主管关系表简单得多而且对于一个扁平化的中小型企业组织架构完全够用。部门表 sys_dept 就四个字段部门ID、部门名称、部门负责人ID、创建时间。这里有一个实践经验部门负责人不要冗余存负责人姓名只存用户ID然后关联查询。如果你把姓名直接冗余进部门表一旦用户改名或者离职同步逻辑就会很麻烦。2.2 考核指标与模板权重控制的核心考核不是拍脑袋打一个总分而是要拆成多个维度。我设计了考核指标表 assess_item 和考核模板表 assess_template 两张表来支撑这个需求。指标表存放的是有哪些可选的考核项CREATE TABLE assess_item ( id INT PRIMARY KEY AUTO_INCREMENT, item_name VARCHAR(50) NOT NULL COMMENT 指标名称, item_type VARCHAR(20) COMMENT 考核维度业绩/能力/态度, item_desc VARCHAR(255) COMMENT 指标说明, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );模板表存的是某个月份实际用了哪些指标以及每个指标的权重CREATE TABLE assess_template ( id INT PRIMARY KEY AUTO_INCREMENT, month VARCHAR(6) NOT NULL COMMENT 考核月份 如202401, item_id INT NOT NULL COMMENT 指标ID, weight DECIMAL(5,2) NOT NULL COMMENT 权重百分比和为100, sort_order INT COMMENT 展示顺序 );为什么要拆成模板表而不是直接把指标和权重写在考核记录里因为企业的考核指标体系是会动态调整的。比如 1 月用了销售额达成率客户满意度两个指标2 月可能新增团队协作指标权重也要重新分配。如果直接在考核记录表里冗余指标名称和权重也能实现但统计每个指标一个月被考核了多少次这种分析时就会很难写 SQL。模板表把每月指标体系单独抽出来考核记录只需要关联模板ID即可。这里补充一个我自己踩过的坑权重之和的校验不要只在前端做。JSP 页面上可以用 jQuery 算出权重总和是否为 100但后端保存模板时也必须校验一次。原因很简单——前端校验可以被绕过而权重错了会直接影响所有员工的最终得分属于基础数据错误必须在入库前拦截。2.3 考核记录表一次打分一条记录考核记录表 assess_record 是这个系统的核心业务表CREATE TABLE assess_record ( id INT PRIMARY KEY AUTO_INCREMENT, template_id INT NOT NULL COMMENT 考核模板ID, assessor_id INT NOT NULL COMMENT 考核人ID主管, assessed_id INT NOT NULL COMMENT 被考核人ID, item_id INT NOT NULL COMMENT 考核指标ID, score DECIMAL(5,2) NOT NULL COMMENT 该项得分百分制, remark VARCHAR(255) COMMENT 考核评语, status INT DEFAULT 0 COMMENT 0-草稿 1-已提交, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );我设计了一个 status 字段区分草稿和已提交这个字段的价值在实际使用中特别大。主管给下属打分通常不是一个早上就能完成的可能在周一打了半截周三局域网断了周五继续打。如果只有保存没有提交状态就会出现主管只打了 3 项指标但因为误操作全部提交后面想改也改不了的尴尬局面。有了状态位草稿可以随便改提交后如需修改必须走管理员重置流程。同时为了防止一个人给自己打分我在 SQL 层面对 assessor_id 和 assessed_id 做了关联校验查询下属时使用 leader_id 匹配且提交时再次判断被考核人的 leader_id 是否为当前登录用户。这种双重校验虽然增加了代码量但是必要的安全底限。2.4 报表统计的 SQL 思路GROUP BY 和子查询的配合绩效考核最容易让人头疼的部分其实是最后的汇总统计。每个人的总分不是存在某一行里的而是要按人、按月把多条考核记录聚合成一条结果。我的统计逻辑是SELECT u.real_name, d.dept_name, SUM(IFNULL(r.score, 0) * t.weight / 100) AS total_score FROM assess_record r JOIN assess_template t ON r.template_id t.id JOIN sys_user u ON r.assessed_id u.id JOIN sys_dept d ON u.dept_id d.id WHERE t.month 202401 AND r.status 1 GROUP BY u.id, d.dept_name ORDER BY total_score DESC;这里有一点要注意MySQL 开启了 ONLY_FULL_GROUP_BY 模式后SELECT 后面出现的非聚合列必须出现在 GROUP BY 里否则会报错。所以我 GROUP BY 了两个字段u.id, d.dept_name而不是只按 u.id 分组。这也是很多新手在 Navicat 里反复报错的原因。3. SSM 整合的核心配置Maven、web.xml、Spring、MyBatis 四件套这套系统最大的拦路虎不在业务代码而在 SSM 三个框架如何整合进一个工程。Spring 管业务BeanSpringMVC 管控制层MyBatis 管数据库它们各司其职但整合过程涉及大量配置文件。我分成四个部分说清楚。3.1 Maven 的 pom.xml版本号是最大的坑Maven 在这个项目里负责两件事依赖管理和构建打包。先看核心依赖properties spring.version5.3.27/spring.version mybatis.version3.5.13/mybatis.version mysql.version8.0.33/mysql.version /properties dependencies !-- Spring核心容器 事务管理 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-tx/artifactId version${spring.version}/version /dependency !-- SpringMVC -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency !-- MyBatis 和 Spring 整合插件 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.2/version /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version${mysql.version}/version /dependency !-- 数据库连接池Druid -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency !-- JSP 和 JSTL -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- Jackson处理 Ajax JSON 转换 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.2/version /dependency /dependencies版本号这里我要多说几句。SSM 项目最常见的 Maven 报错就是 jar 包冲突或者 NoSuchMethodError绝大多数都是版本不匹配导致的。我第一次搭建的时候就踩了 mysql-connector-java 5.x 和 MySQL 8.x 认证方式不兼容的问题。MySQL 8 默认使用 caching_sha2_password 认证老驱动连不上。所以这里直接用 mysql-connector-j 8.0.33配置连接串时别忘了加上useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue。尤其是 allowPublicKeyRetrieval不加这个参数连接 MySQL 8 时有时候会报 Public Key Retrieval is not allowed这是新手最常遇到的数据库连接报错之一。另外javax.servlet-api 一定要设为 provided 作用域。否则你打出来的 war 包里会包含 Servlet API 的 class和 Tomcat 自带的产生冲突部署后会出现各种奇奇怪怪的错误。3.2 web.xmlDispatcherServlet 与编码过滤器SSM 整合的起点是 web.xml。我在这套系统里配置了四个关键部分ContextLoaderListener 加载 Spring 容器、DispatcherServlet 加载 SpringMVC 容器、CharacterEncodingFilter 解决中文乱码、欢迎页和控制静态资源放行。context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping servlet servlet-namedispatcherServlet/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcherServlet/servlet-name url-pattern//url-pattern /servlet-mappingforceEncoding 设置为 true 非常关键。如果我写成valueUTF-8/value而不是valuetrue/value某些 Tomcat 版本下请求参数仍然会出现乱码。这个配置的目的是不管请求和响应原本的编码是什么强制都使用 UTF-8。尤其是 GET 请求参数Tomcat 8 默认 URIEncoding 已经是 UTF-8但为了保险我还会在 server.xml 里的 Connector 配置中手动加上URIEncodingUTF-8双重保障彻底解决中文乱码。3.3 Spring 容器配置业务层和事务管理applicationContext.xml 是 Spring 的根容器配置我在这里只扫描 Service 层和 Mapper 层不扫描 Controller。因为 Controller 归 SpringMVC 管如果两边都扫描会导致同一个 Bean 被创建两次事务注解和 AOP 有时会失效。context:component-scan base-packagecom.example context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan !-- 数据源 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/kpi_system?useSSLfalseamp;serverTimezoneAsia/Shanghaiamp;allowPublicKeyRetrievaltrue/ property nameusername valueroot/ property namepassword value123456/ property nameinitialSize value5/ property namemaxActive value20/ /bean !-- SqlSessionFactory -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.example.entity/ /bean !-- Mapper 扫描 -- bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.mapper/ property namesqlSessionFactoryBeanName valuesqlSessionFactory/ /bean !-- 事务管理器 -- bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/xml 配置里的amp;转义是个小细节。在 XML 中 符号必须写成 否则解析器会报错。很多人在 applicationContext.xml 里写连接串时漏了这一步启动时直接抛 SAXParseException。Druid 连接池在这里不仅是性能优化更重要的是它自带监控页面可以在开发阶段通过 Druid 的 Web 界面查看慢 SQL、活跃连接数排查问题比裸 JDBC 方便太多。3.4 spring-mvc.xml视图解析器与静态资源SpringMVC 的配置相对简单但有两个点必须注意JSP 视图解析器要加 JSTL 支持以及静态资源js、css、图片要显式放行。context:component-scan base-packagecom.example.controller/ mvc:annotation-driven/ !-- 视图解析器 -- bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean !-- 静态资源放行 -- mvc:resources mapping/static/** location/static//为什么把 JSP 放在 /WEB-INF/views/ 下而不是 WebContent 根目录因为 /WEB-INF 目录下的文件不能被浏览器直接通过 URL 访问。用户只能通过 Controller 跳转到 JSP这样在做权限控制时可以保证所有页面都必须先经过登录拦截器的检查避免有人绕过登录直接访问某个页面。这是一个安全上的最佳实践虽然多写了几行 Controller 代码但值得。3.5 MyBatis 映射文件resultMap 与动态 SQLMyBatis 这块的核心是 mapper XML。一套成熟的 SSM 项目里最不能省的就是 resultMap 的配置。拿考核记录联查用户信息来说如果不用 resultMap就需要在 SQL 里起别名代码里再手动封装既麻烦又容易出错。resultMap idRecordVO typecom.example.vo.AssessRecordVO id propertyid columnid/ result propertyassessedName columnreal_name/ result propertydeptName columndept_name/ result propertyitemName columnitem_name/ result propertyscore columnscore/ result propertymonth columnmonth/ /resultMap select idselectRecordsByAssessor resultMapRecordVO SELECT r.id, u.real_name, d.dept_name, i.item_name, r.score, t.month FROM assess_record r LEFT JOIN sys_user u ON r.assessed_id u.id LEFT JOIN sys_dept d ON u.dept_id d.id LEFT JOIN assess_template t ON r.template_id t.id LEFT JOIN assess_item i ON t.item_id i.id WHERE r.assessor_id #{assessorId} AND r.status 1 if testmonth ! null and month ! AND t.month #{month} /if ORDER BY t.month DESC, r.id DESC /select动态 SQL 的if标签要配合where或者手写 WHERE 11 使用。在这里我用了if但没写where因为 WHERE 条件本身存在后面的动态条件用 AND 连接是安全的。但如果你全部都是动态条件最好用where标签它可以智能去掉多余的 AND。4. 核心功能模块的实现登录、权限拦截、考核打分、统计报表框架整合完毕项目能不能跑起来就看业务功能了。我挑了登录与权限、考核打分、统计报表三个最核心的功能讲述。这三个功能分别代表了 SSM 项目的三种典型场景会话管理与拦截器、表单提交与事务、复杂查询与展示。4.1 登录与权限拦截HandlerInterceptor 的奇效登录不是简单地比对一下用户名密码就完事。我设计了一个 LoginInterceptor把登录检查和权限校验统一放在拦截器里处理而不是在每个 Controller 方法里重复写 if 判断。这样代码干干净净逻辑也集中在同一个地方。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { // 未登录跳转登录页 response.sendRedirect(request.getContextPath() /login); return false; } // 角色权限判断可根据 handler 方法上的自定义注解做细粒度控制 return true; } }然后需要在 spring-mvc.xml 里注册拦截器并配置拦截路径mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/ mvc:exclude-mapping path/static/**/ bean classcom.example.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors这里要注意拦截路径的写法。如果你把 /login 也拦截了那么用户永远也无法登录因为登录请求直接会被打回登录页造成死循环。静态资源也一定要排除否则 CSS、JS 都加载不出来页面会变成一堆纯文本。密码的存储和校验我用了 MD5 固定盐值。实际生产环境中更推荐 BCrypt但对这个规模的内部系统MD5 加盐已经能挡住绝大多数风险。前端提交密码时先用 MD5 散列一次后端拿到后再加盐散列和数据库比对这样即使数据库泄露原始密码也不会直接暴露。4.2 考核打分一次 Ajax 提交保存多条记录考核打分的业务逻辑是这样的主管在该考核月份选择一名下属页面上会动态加载该月的考核模板比如工作业绩-40分、工作能力-30分、工作态度-30分主管针对每个指标给出分数并附加评语最后点击提交。这个功能如果按传统方式写就是提交一个表单后端接收多个同名参数然后循环插入。但我在实际开发中选择了 jQuery Ajax 提交 JSON 数据让前后端交互更灵活。前端把整个打分项组装成 JSON 数组后端用 List 接收PostMapping(/score/submit) ResponseBody public ResultVO submitScore(RequestBody ListAssessRecord records, HttpSession session) { User assessor (User) session.getAttribute(loginUser); // 校验不能给自己打分、被考核人必须是下属、分数不能超过100 for (AssessRecord record : records) { record.setAssessorId(assessor.getId()); record.setStatus(1); } assessRecordService.saveBatch(records); return ResultVO.success(提交成功); }批量插入在 MyBatis 里用 foreach 标签实现一条 INSERT 语句插入多条记录比循环调用单条插入性能高出很多insert idinsertBatch INSERT INTO assess_record (template_id, assessor_id, assessed_id, item_id, score, remark, status) VALUES foreach collectionlist itemr separator, (#{r.templateId}, #{r.assessorId}, #{r.assessedId}, #{r.itemId}, #{r.score}, #{r.remark}, #{r.status}) /foreach /insert这里事务保护是必须的。saveBatch 方法上要加 Transactional 注解因为多条 INSERT 要么全部成功要么全部失败。如果有一条插入失败但前面的成功提交了就会出现考核记录不完整的数据脏状态主管提交后员工看到的分数是缺项的。前端打分页面的校验逻辑我用 jQuery 实现先判断每个分数是否在 0-100 范围再判断是否所有指标都已经打分最后弹出确认框。确认框用 Bootstrap 的模态框实现比原生 confirm 好看得多也更符合企业内部系统的视觉习惯。4.3 统计报表ECharts 与原生表格双轨展示统计报表完成后会直观地呈现出每个人的总分、排名、所在部门平均分。我采用了两种展示方式排名表格 柱状图。表格用 JSP JSTL 渲染图表用原生的 Canvas 手写柱状图避免引入太多额外依赖这样既照顾了想快速查阅的管理员也满足了想直观对比的管理层需求。表格部分在 JSP 页面里用 JSTL 的 c:forEach 循环输出c:forEach items${scoreList} varitem varStatusst tr td${st.count}/td td${item.realName}/td td${item.deptName}/td td${item.totalScore}/td td c:if test${item.totalScore 90}A优秀/c:if c:if test${item.totalScore 80 item.totalScore 90}B良好/c:if c:if test${item.totalScore 60 item.totalScore 80}C合格/c:if c:if test${item.totalScore 60}D待改进/c:if /td /tr /c:forEach这里用 EL 表达式直接访问 JavaBean 属性配合 JSTL 的 c:if 做等级判定代码看着就很清爽。很多新手喜欢在 JSP 里写大段 Java 脚本片段% ... %这是最不推荐的写法。JSP 脚本片段会让页面和后端逻辑严重耦合后期维护极其痛苦。正确的做法是把所有业务判断放在 Controller 或 Service 里JSP 只负责展示。5. 前端页面JSP 动态渲染 Bootstrap 布局 jQuery 异步交互的三层配合前端这块很多纯后端程序员容易忽略但实际上一个企业管理系统好不好用前端占了一半的功劳。我把页面拆成布局模板和功能页面两层来实现。5.1 布局模板避免每个 JSP 重复写导航栏我建了一个 common/header.jsp 公共片段里面是 Bootstrap 的顶部导航和左侧菜单。每个页面只需要在 JSP 开头% include filecommon/header.jsp %引入就能自动获得统一样式。这个做法的好处是改导航菜单只需要改一个文件全站同步生效不用像无头苍蝇一样去每个页面找菜单链接。菜单的展示还可以区分角色。比如管理员能看到指标管理模板管理用户管理菜单主管只能看到考核打分我的下属员工只能看到我的成绩。这个区分用 JSTL 判断 session 里的用户角色来渲染c:if test${sessionScope.loginUser.role 1} lia href${pageContext.request.contextPath}/admin/user/list用户管理/a/li /c:if5.2 Bootstrap 的栅格布局与表单美化Bootstrap 在这套系统里解决的是响应式布局和基础组件样式。我主要用了它的栅格容器、表格样式、按钮样式、表单控件校验样式这几类。拿打分页面来说一个指标对应一行每行是一个 form-group左侧是指标名和权重说明右侧是分数输入框。用 Bootstrap 的栅格分列后在大屏幕上是两列并排在窄屏幕比如管理员用平板演示会自动折行不需要额外写媒体查询。表单校验状态的反馈也是 Bootstrap 的强项。我在输入框上加了 is-invalid 类名配合 jQuery 在失焦事件里校验分数范围不给过就红色警告框提示。这种交互体验虽然基础但对内部系统来说完全够用而且代码量很少。5.3 jQuery 的 Ajax 交互局部刷新代替整页跳转Bootstrap 管静态样式jQuery 管动态交互。这套系统的异步场景主要有三个登录验证、打分提交、筛选报表月份。三者我都用了$.ajax举打分提交的例子$(#submitBtn).click(function () { var records []; $(.score-item).each(function () { var itemId $(this).data(item-id); var score $(this).val(); var remark $(this).siblings(.remark-input).val(); records.push({ templateId: $(#templateId).val(), itemId: itemId, score: score, remark: remark }); }); $.ajax({ url: contextPath /score/submit, type: POST, contentType: application/json, data: JSON.stringify(records), dataType: json, success: function (res) { if (res.code 200) { alert(提交成功); window.location.reload(); } else { alert(res.msg); } }, error: function () { alert(网络异常请重试); } }); });这里有个容易犯的错误请求头里没有设置Content-Type: application/json后端用 RequestBody 接收 List 时直接报 HttpMediaTypeNotSupportedException。记得在 ajax 里显式写上 contentType这是前后端对接时最常见的错。另外我在每个页面都需要获取项目的 contextPath也就是部署路径。如果是根路径部署可以直接写 /kpi_system/xxx但为了迁移到不同环境时不出问题我在 JSP 里用${pageContext.request.contextPath}输出到了 JavaScript 变量里然后所有请求都用这个变量拼接。这是一个很不起眼但很实用的习惯。6. 权限控制的三个角色菜单、接口、数据三个层面分别拦什么企业内部系统最怕什么最怕员工越权。一个普通员工如果通过篡改 URL 直接访问了用户管理页面或者在打分接口里把自己指定为考核人整个系统就乱套了。所以这套系统的权限控制分三层做。6.1 菜单层不同角色看到的菜单不同菜单层只影响体验不影响安全。它的作用是让普通员工根本看不到用不着的入口减少误操作的可能。这一层就是前面说的 JSTL 判断角色渲染菜单。登录之后把用户对象放入 sessionJSP 里通过 sessionScope 取出来做判断。菜单权限的实现代码量少但非常直观。6.2 接口层拦截器拦截非法请求接口层是安全的关键。SpringMVC 拦截器不仅检查登录状态还可以做接口级别的角色校验。我在项目中自定义了一个 RequireRole 注解标注在 Controller 方法上然后扩展 LoginInterceptor 读取注解并校验当前用户是否具备角色。Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { int value(); }Controller 方法上这样使用RequireRole(1) GetMapping(/admin/user/list) public String userList(Model model) { ... }拦截器的 preHandle 里通过 HandlerMethod 拿到注解做角色比对if (handler instanceof HandlerMethod) { HandlerMethod hm (HandlerMethod) handler; RequireRole requireRole hm.getMethodAnnotation(RequireRole.class); if (requireRole ! null user.getRole() ! requireRole.value()) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 跳转到无权限提示页 return false; } }这一层到位后即使前端的菜单被绕过后端接口也会拦截不合法的访问。6.3 数据层只能操作自己权限范围内的数据数据层是最后一层兜底。比如主管给下属打分时记录里的 assessor_id 必须取 session 中的当前用户而不是前端传过来的值。我在代码里强制设置了 record.setAssessorId(assessor.getId())前端就算伪造 Json 传一个 assessor_id 进来也会被覆盖。再比如员工查询自己的成绩时SQL 里强制带上了 assessed_id 当前用户ID 的条件而不是把所有成绩都查出来再过滤。这种服务端信任边界的思路比只依赖前端传参要安全得多。7. 打包部署与常见报错从 IDEA 到 Tomcat 的最后一公里代码写完最后一步是把项目部署起来。很多同学卡在这一步比如 JSP 编译报错、数据库连不上、依赖没有打进 war 包。我把自己部署这套系统时的完整流程和报错表现写出来。7.1 用 Maven 打 war 包的三种方式在 IDEA 里打 war 包有三种方式IDEA 的 Maven 工具窗口双击 package、命令行 mvn clean package、配置 Tomcat 直接热部署。我最推荐第三种因为它能把 debug 和部署一次完成。热部署配置方式IDEA 的 Run/Debug Configurations 里新增 Tomcat Server选择 Local在 Deployment 标签页添加 Artifact选择 war explodedApplication context 设置为 /kpi_system。这样每次改完代码点一下 debug 按钮IDEA 会自动编译和部署不需要手动重启 Tomcat。如果项目里的 JSP 改了甚至不需要重启 Tomcat因为 JSP 是动态编译的只要把改动的 JSP 文件拷贝到 Tomcat 的工作目录即可。这也是 JSP 项目在开发阶段比前后端分离项目方便的地方。7.2 部署到 Linux 服务器的步骤正式部署时把打好的 war 包上传到 Tomcat 的 webapps 目录下启动 Tomcat 后它会自动解压。如果不想自动解压可以在 conf/server.xml 的 Host 里配置 autoDeploy 属性或者手动 mkdir 建目录再放文件。数据库侧的部署步骤更值得注意。MySQL 这边建议先用 SQL 脚本建好库和表然后把连接的 IP 从 localhost 改成服务器实际 IP因为 Tomcat 在本机MySQL 在本机localhost 没问题但如果你是远程连别人的 MySQL连接串里的主机名一定要改。这个细节经常让人排查半天。7.3 常见异常清单复现表现与解决方案SSM 项目部署阶段的报错信息又长又抽象我整理了一份高频异常清单基本都是我实际遇到过并解决过的报错关键信息出现位置可能原因解决方案ClassNotFoundException: com.mysql.cj.jdbc.Driver启动时mysql 驱动依赖缺失检查 pom.xml确保 mysql-connector-j 已引入且打包Public Key Retrieval is not allowed启动时MySQL 8 认证特性连接串加 allowPublicKeyRetrievaltrueInvalid bound statement (not found)调用 Mapper 时Mapper XML 没扫描到检查 mapperLocations 路径与 XML namespaceNo qualifying bean of type XxxMapper注入时Mapper 扫描包设置不对检查 MapperScannerConfigurer 的 basePackage前端 404路径报错请求时context path 不对优先使用 ${pageContext.request.contextPath} 拼接jasper 编译 JSP 报错启动时缺少 JSP 编译支持引入 tomcat-embed-jasper 依赖或使用完整 Tomcat静态资源 404页面访问没有 mvc:resources 放行spring-mvc.xml 配置 static 资源映射这个清单我建议收藏起来。遇到问题时不要盲目改配置先对照报错信息缩小排查范围。绝大多数 SSM 项目启动失败原因都逃不出这几类。8. 几个值得留意的优化方向系统跑通之后我又做了几个小的优化坦白说这些优化不算特别高深但对内部系统的实际使用体验提升明显。第一个是考核周期的自动初始化。每个月 1 号系统需要生成新的考核模板手动创建很麻烦。我在 Service 层写了一个定时任务的逻辑用 Spring 自带的 Scheduled 注解每月自动复制上个月的指标模板生成新模板主管登录后直接就能看到本月的打分任务。这里要说明的是Scheduled 在 SSM 项目里启用需要在 spring 配置文件中加上task:annotation-driven/否则注解不生效。第二个是考核进度展示。主管界面上我加了一个进度条显示已打分数/应打分数。这个统计通过一条 SQL 分组查询完成SELECT assessed_id, COUNT(*) FROM assess_record WHERE assessor_id? AND status1 GROUP BY assessed_id。然后在前端用 Bootstrap 的进度条组件展示百分比。有了这个功能主管就不会出现月底漏打分的情况。第三个是 Excel 导出。绩效系统做完以后HR 必然会要求能不能导出一个 Excel。用 jQuery 表格转 CSV 是最轻量的方案我在这套系统里是通过后端生成 CSV 文件输出到前端下载用 response.setContentType 设置 application/csv文件名带上时间戳。这个功能扩展起来很容易Java 后端生成 CSV 就几行代码的事。9. 写在最后这套 JavaWeb SSM 企业绩效考核管理系统从数据库设计到 SSM 整合、从页面渲染到权限控制全流程走下来我最大的体会是框架最终只是工具真正考验人的是对业务的理解和对数据流转的把控。SSM 这套技术栈虽然老但正因为老网上你能搜到的每个报错基本都有人踩过、有解决方案学习成本反而低。如果看到这里的你也正在做类似的项目我建议你不要只满足于把代码跑起来。试着回答三个问题为什么考核记录表要保留草稿状态为什么权限拦截要放在后端而不是依赖前端隐藏菜单为什么 MySQL 8 的连接串要加那么多参数把这三个问题想透了你对这套技术栈的理解就超过大多数只会在网上找 Demo 的人了。最后再分享一个很实用的技巧开发 SSM 项目时在配置数据源时多打一行logImpl设置把 MyBatis 的 SQL 日志打开可以通过 log4j 配置 TRACE 级别输出 SQL。开发阶段每一个查询都可以在控制台看到实际执行的 SQL 和参数排查问题效率至少提升一倍。生产环境再关掉也不迟——这种开发阶段看得见 SQL的习惯比什么高级调优都实用。
返回列表