ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SSM+MySQL企业OA系统开发实战:从架构到审批模块设计

SSM+MySQL企业OA系统开发实战:从架构到审批模块设计

每一个做过毕设的人,大概都逃不过“XX管理系统”这个宿命。我当年也接过一个类似的活:基于javaweb和mysql的ssm公司企业oa管理系统(java+ssm+jsp+javascript+mysql)。说实话,这种题目光看名字就知道要干什么了——做一个能让公司员工在线办公的系统,包括登录、员工管理、公告、审批、日程这些零碎又必须的功能。

但真正上手之后才发现,这个项目最难的不是“写代码”,而是“把SSM框架和文化课里讲的那套三层架构,踏踏实实落到一个真实系统里”。JSP、JavaScript、MySQL这些东西单独拎出来都能看懂,一旦组合在一起,各种配置、依赖、路径问题就会排着队来找你。这篇博文,我想把这套系统的搭建思路、核心模块设计、数据库表关系,以及我踩过的那些坑,完整地梳理一遍。

如果你也正在写类似的管理系统,或者想搞清楚SSM整合到底是怎么回事,这篇文章应该能帮你少走不少弯路。

1. 为什么要用SSM做OA系统,以及整体架构怎么摆

1.1 SSM这套组合在OA系统里分别扮演什么角色

很多同学看到SSM的第一反应是“三个框架拼一起好高级”,其实拆开看就非常直观了。Spring是容器,负责管理系统里的各种Bean,尤其是Service层和DAO层的实例管理以及事务控制;SpringMVC负责接收浏览器发来的请求,把URL路由到具体的Controller方法,然后再把返回的数据渲染到JSP页面;MyBatis负责和MySQL打交道,把SQL语句写在XML或注解里,Mapper接口调用后就返回我们需要的对象或List。

对应到OA系统里,就是这样的分工:用户在浏览器输入地址,请求进Tomcat,SpringMVC的前端控制器DispatcherServlet把请求转发给对应的Controller,Controller调Service接口处理业务逻辑,Service内部调Mapper接口执行数据库操作,Mapper的XML里写实际SQL语句,操作MySQL里的表。

这套链路里有一个概念不能搞混:JavaWeb里常说的Servlet,在SSM项目中并不会一张张去写。SpringMVC本身就是基于Servlet封装的,它的DispatcherServlet已经在web.xml里注册好了,所有请求其实都是先经过它,再由它分发给Controller。很多初学者在这里钻进死胡同,拿着以前写的Servlet代码硬往里塞,结果越改越乱。记住:SSM项目里,Controller替代了Servlet的绝大多数职责。

1.2 包结构怎么设计,决定了后面写代码的舒不舒服

搭建项目之前,包结构一定要先规划好。我见过不少同学把所有的类都丢在com.example.demo一个包下面,类多了以后连自己都找不到。按照SSM的标准做法,至少要拆成下面几层:

  • entity(或者叫pojo/domain):存放数据库表对应的实体类,属性就是表的字段。
  • mapper(或者叫dao):存放MyBatis的Mapper接口,只写方法声明。
  • service:存放业务逻辑接口,以及impl包下的实现类。
  • controller:存放处理请求的类,负责接收参数、调用service、返回视图。
  • config(或者叫config包放配置类,如果走注解配置的话;传统XML配置项目则不强制)。
  • interceptor:存放拦截器,登录拦截基本都会用到。
  • common(或者叫util):放公共工具类,比如MD5加密、日期处理等。

分组明确之后,写包名时也要避免脑袋一热就取奇怪的名字。包名一般推荐反域名倒写加项目名,例如com.company.oa。公司名用虚构代称就行,关键是层级清晰——“考试前临时改包名”这种事一次就够教训了,改完以后一堆import全红,光是修依赖关系就能修到怀疑人生。

2. OA系统的功能模块怎么拆,审批流怎么落表

2.1 业务模块规划:一个够用的OA至少得有这些

拿着“企业OA”这个题目,功能不能太少,但也不能贪多,否则答辩时根本维护不过来。我当时规划的模块如下,你可以照着调整:

  • 登录模块:用户名密码验证、验证码、Session保存当前用户、登录拦截。
  • 员工管理:员工增删改查,分页搜索,维护用户所属部门和职位。
  • 部门管理:部门信息维护,树状层级或列表展示均可。
  • 公告管理:发布公告、查看公告列表、公告详情,通常只有管理员能发布和删除。
  • 审批管理:请假申请、报销申请,提交后审批人审批,状态流转。
  • 日程管理:维护个人日程,提供简单的增删改查。
  • 个人中心:登录用户查看和修改自己的基本信息、改密码。

这个功能清单看起来很常规,但每一个模块都能继续挖出问题。比如员工管理,到底谁有权限新增员工?部门经理能不能查询自己部门下的员工?审批流程里,能不能指定审批人?这些问题如果前期不界定清楚,写代码时很容易摇摆不定,最后做出一个“四不像”的系统。

如果只做管理端,最简单的人事权限设定是:系统初始化一个超级管理员账号,可以把其他用户设置为管理员;普通员工只能查看公告、提交审批、维护自己的日程和资料;管理员用户则拥有全部模块的增删改查权限。这种权限模型虽然简单,但足够覆盖一个二期可扩展的OA系统。先跑通,再优化,是这种项目最务实的路线。

2.2 审批模块的表结构设计:状态机思维是关键

审批模块是OA的核心亮点,做好了答辩时绝对有东西可讲。请假审批正常涉及一个申请表和一个审批记录表。申请表主要字段包括申请编号、申请用户ID、请假类型、开始时间、结束时间、请假事由、当前状态、创建时间;审批记录表则包括记录编号、申请编号、审批人ID、审批结果、审批意见、审批时间。

审批状态我一般用整数表示:0是待审批,1是同意,2是驳回,3是已撤销。为什么不直接用字符串“待审批”“同意”?因为字符串在比较大小、统计计数时不好处理,而且一旦打字不一致,比如“同意”和“通过”,数据就乱了。用整数常量,在Java里定义成全局静态变量,页面和Service都引用同一份,方便又不容易出错。

审批流程里的“下一步动作”也要提前想清楚。员工提交请假后,审批人打开申请详情,能看到申请人的名字、部门和请假时长;点击同意或驳回时,需要填审批意见,同时把申请表的状态改成1或2,并插入一条审批记录。如果同一个申请需要“先部门经理审批、再人事审批”,那就要再加一个审批层级字段,比如当前审批级别,配合一条从低到高的审批链。第一次做OA的同学不建议一下子做多级审批,流动性太大,可以先固化一个审批人,等流程稳定之后再加多级审批逻辑。

2.3 公告和日程这些“小模块”,怎么做才不会显得敷衍

公告模块常见的做法就是一张公告表,字段有公告编号、标题、内容、发布人ID、发布时间、是否置顶。查看公告列表时按时间倒序,置顶的排在最前面。这不算复杂,但有几个细节要注意,比如发布时间最好直接在Java代码里用new Date()生成,不要用数据库时间函数,否则写代码时容易因为时区问题遇到一些意想不到的时间偏差。

日程模块我建议把字段控制得精简一点:日程编号、用户ID、标题、开始时间、结束时间、提醒方式(可不做)、备注。页面就是一个列表页加新增、编辑、删除弹窗。日程的时间选择器不要在JavaScript里自己实现,直接用现成的日期控件,比如原生datetime-local或者引入插件,能节省大量时间。做这类管理系统的核心思路其实不是炫技,而是“能用、稳定、逻辑自洽”。

3. 数据库设计:OA系统的表要这样设计才顺

3.1 核心表都有哪些,关联关系怎么理

我拿到题目后第一件事不是写代码,而是设计数据库表。项目如果连表都没定清楚,后面写SQL必然各种返工。基于上面的功能模块,我当时建了这几张核心表:

  • employee(员工表)
  • department(部门表)
  • notice(公告表)
  • leave_apply(请假申请表)
  • reimburse_apply(报销申请表)
  • audit_record(审批记录表)
  • schedule(日程表)
  • menu(菜单表,如果要做简单权限菜单,可选用)

表和表之间的关系也相对清晰:employee表通过dept_id关联department表;leave_apply通过user_id关联employee表;audit_record通过apply_id和apply_type来区分审批的是请假还是报销,同时记录审批人的ID。这里有一个容易踩的坑:如果一张审批记录表同时服务请假和报销,那么apply_id不能直接设成某一张表的外键,因为它在两张表里都有对应。解决方式是增加一个apply_type字段,比如1表示请假申请,2表示报销申请,查询时用apply_type加apply_id一起定位。

3.2 员工表和审批表的具体SQL怎么设计

先看员工表的设计思路。密码通常用MD5加盐存字符串,因此在MySQL中我们直接存varchar类型,长度32位或更长。角色字段可以用role_id区分超级管理员和普通员工。字段如下:

CREATE TABLE employee ( id INT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) UNIQUE NOT NULL, name VARCHAR(30) NOT NULL, password VARCHAR(64) NOT NULL, gender TINYINT DEFAULT 1, phone VARCHAR(11), email VARCHAR(50), dept_id INT, position VARCHAR(30), role_id TINYINT DEFAULT 0, status TINYINT DEFAULT 1, create_time DATETIME );

请假申请表的设计则要覆盖业务状态流转的需求。要记得预留一个remark字段记录驳回意见的摘要,避免每次查询都要去审批记录表里翻半天。

CREATE TABLE leave_apply ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, leave_type VARCHAR(20), start_time DATETIME, end_time DATETIME, days DECIMAL(5,1), reason VARCHAR(255), status TINYINT DEFAULT 0, approver_id INT, apply_time DATETIME );

为什么不把days字段设成INT?因为请假的时长很可能不足一天,比如请假半天就是0.5天。DECIMAL(5,1)最多支持四位整数加一位小数,对于企业假期管理完全够用。计算时长时,用end_time减去start_time得到毫秒数,再除以每天的毫秒数86400000,保留一位小数,就是请假天数。不要图省事在前端写死天数,因为前端改时间后如果不重新计算,提交的数据就会自相矛盾。

3.3 MyBatis的XML映射和动态SQL小技巧

实体类和表字段的映射,我建议在mybatis-config或者xml里显式写好resultMap。虽然MyBatis默认开启驼峰转换,但多表联查时,比如查询员工表关联部门表列出部门名称,返回的结果里带一个deptName字段,就必须靠resultMap或者as别名来对应。写SQL的规范是:字段再多也要明确列出,不要用select *。使用动态查询时,可以用where标签加if标签拼接条件,MyBatis会自动处理多余的空格和and。

举一个查询员工列表的例子:

<select id="selectEmployeeList" resultMap="EmployeeResultMap"> SELECT e.*, d.name AS dept_name FROM employee e LEFT JOIN department d ON e.dept_id = d.id <where> <if test="name != null and name != ''"> AND e.name LIKE CONCAT('%', #{name}, '%') </if> <if test="deptId != null"> AND e.dept_id = #{deptId} </if> </where> ORDER BY e.id DESC </select>

这种写法最大的好处是安全。直接用字符串拼接SQL,用户输入一点特殊内容就可能把整个查询条件破坏掉,而用#{}占位符传参,MyBatis会把它编译成PreparedStatement的参数,从根源上解决SQL注入的问题。这一点在进入企业开发后也依然是底线级别的安全要求。

4. SSM整合配置实操,这一步比写代码更考验耐心

4.1 工程骨架和Maven依赖怎么准备

我用的是Maven工程。打开IDEA新建一个Maven项目,然后打上war包类型。这一步别嫌麻烦,Maven能帮你管理依赖和构建,省掉手动下载jar包的时间。pom.xml里放哪些依赖,每个版本选什么,这些我踩过的版本坑相当多,特别是Spring和JDK版本不对时,经常冒出一些莫名其妙的错误。下面是一个稳定的组合参考:

  • Spring 5.2.x 系列
  • SpringMVC 5.2.x 系列(和Spring版本保持一致)
  • MyBatis 3.5.x
  • mybatis-spring 2.0.x
  • MySQL驱动 8.0.x(如果你的MySQL是5.7,用5.1.49也可以)
  • 连接池,比如druid或者c3p0
  • JSTL 1.2
  • Jackson(处理AJAX的JSON数据)
  • Servlet API 4.0.1、JSP API 2.3.0(scope为provided)
  • lombok(可选,看个人习惯)

pom依赖的核心要提醒一点:Spring和SpringMVC的版本如果不同步,会出现EmptyDispatcherServlet或者找不到类的情况。初学者最好不要用JDK 17搭配太老的Spring版本,否则编译阶段就会出现一大堆报错,而且网上找到的方案往往又不兼容你的环境。我用的是JDK 8和Tomcat 8.5,这套组合在SSM项目里最稳。

4.2 四大核心配置文件的要点

SSM项目里默认需要配置的文件有web.xml、spring-mvc.xml、spring-mybatis.xml(也有的喜欢合并成applicationContext.xml)、mybatis-config.xml、jdbc.properties。配置文件多的好处是结构清楚,缺点是写错一个标签就可能让整个容器启动失败。

web.xml要做三件事:注册DispatcherServlet、配置Spring的ContextLoaderListener监听器、配置CharacterEncodingFilter字符编码过滤器。这三件事缺一不可。字符编码过滤器一定要放在最前面,否则你后面配任何请求都会出现中文乱码。曾经我调试一个乱码问题调试了一个多小时,最后才发现是过滤器没配置,前端传回来的中文全变成了问号。

spring-mvc.xml的重点是开启注解驱动、扫描controller包、配置视图解析器,以及放行静态资源。视图解析器的前后缀设置很直白,比如:

<property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/>

以后Controller里返回一个字符串"login",SpringMVC就会把它对应到WEB-INF/views/login.jsp。把JSP放在WEB-INF下面还有个好处:用户不能直接通过地址栏访问这些JSP,只能通过Controller转发回去,既安全又能强制遵循MVC的流程。

spring-mybatis.xml里要接入数据源、SqlSessionFactory和Mapper扫描。数据源用Druid的话,配置一把jdbc.properties就能把四要素全读进来。SqlSessionFactoryBean里要指定typeAliasesPackage让MyBatis自动给实体类起别名,同时指定mapperLocations到classpath:mapper/*.xml,这样才能把接口和XML映射文件关联起来。

4.3 JSP和JavaScript的分工,哪些事不该前端做

JSP在SSM项目里既承担了页面展示,又可以通过EL表达式和JSTL标签直接读取后端传来的数据。Controller返回ModelAndView或者用Model传参时,JSP里就能直接用${list}拿到结果。遍历列表用c:forEach,判断用c:if,这些都是基础操作,但能显著减少Java代码在页面里的出现频率。

JavaScript主要负责表单校验、AJAX交互、弹窗控制。要注意,前端校验只是用来提升用户体验的,后端Service里必须再做一套数据校验。比如新增员工时,员工工号不能为空、工号不能重复,这两个校验如果只在JS里做,别人直接拿API请求就能绕过校验,把脏数据直塞进数据库。我在项目里通常会在Service层写一个check方法,结合MyBatis的count查询判断数据是否已存在,返回boolean给Controller。

关于AJAX,我一般在需要局部更新的时候用,比如删除一条公告后刷新列表、提交请假时提示成功而不跳转页面。用JQuery的$.ajax或者$.post就能搞定,不需要引入太复杂的前端框架。接口返回JSON时,Controller的方法上需要加@ResponseBody,这个注解和Jackson依赖是配合使用的,少了任何一样,前端拿到的都可能是一堆下载内容而不是JSON数据。

5. 开发阶段的高频问题,以及我的排查手记

5.1 从404到500:最常见的问题怎么定位

如果你在启动项目或者点击某个功能时遇到404,不要急着怀疑自己的代码逻辑。404通常代表路由没匹配上,排查顺序应该是:先看浏览器访问的URL,再看Controller类上的@RequestMapping值,再看方法上的@RequestMapping值,最后检查SpringMVC扫描controller包的路径是否覆盖到这个类。我遇到过最典型的一种情况是Controller类明明写好了,但是包扫描的范围只到Controller目录的上层,导致类没有被Spring装配,URL自然匹配不上。

500错误则是程序运行中抛了异常,最简单的定位方法是看Tomcat命令行日志中的异常堆栈。初学者很容易因为信息太多而忽略最关键的那一行,其实只要找“Caused by”后面的内容就行,那才是导致问题的根因。如果日志里出现“Invalid bound statement”,意思就是Mapper接口的方法在XML文件里找不到对应的SQL。检查一下namespace是否等于接口全限定名,以及标签的id是否和接口方法名一致。

5.2 乱码、事务不生效、Session失效:这些细节决定答辩成败

中文乱码问题,是我见过这个类型项目里出现频率最高的问题。乱码不是单一原因造成的,得查三条链路:浏览器到Tomcat的请求编码、Tomcat到Java代码的编码、Java代码到数据库的编码。web.xml里配了CharacterEncodingFilter只解决第一段,JSP页面本身的contentType和pageEncoding也要是UTF-8,数据库连接URL上要加上useUnicode=true&characterEncoding=UTF-8,数据表和字段的collation也要设置成utf8_general_ci或utf8mb4_general_ci。四条链路全部对齐,乱码才能彻底解决。

事务不生效的问题也坑过不少人。配置文件中开启事务要记得在spring-mybatis.xml里加上<tx:annotation-driven transaction-manager="transactionManager"/>,同时在Service实现类或方法上使用@Transactional。如果你的配置正确,但写操作出错了数据还是被写进数据库,大概率是Spring没有扫描到Service的实现类,导致事务管理器没有被织入。还有一种很隐蔽的情况:同类内部方法调用另一个带@Transactional的方法,事务会失效。因为事务基于代理实现,只有外部调用才会经过代理。

Session失效这块,主要是登录拦截器的逻辑问题。登录拦截器里判断Session中如果没找到“当前登录用户”这个key,就重定向到登录页,这个逻辑没问题。但要注意拦截器的放行范围,比如登录接口本身、验证码接口、静态资源、JSP资源都要排除掉,否则会出现一个死循环:登录请求被拦截,跳转登录页又被拦截,最后浏览器提示重定向次数过多。放行路径的匹配规则要写仔细,宁可多放行一些静态资源,也别卡掉登录请求。

5.3 前端调接口报错,怎么利用开发者工具排查

做前后端交互的项目,浏览器按F12打开开发者工具已经是最基本的调试姿势。Network面板的用途是看请求是否发出、返回什么HTTP状态码、请求体和响应体是什么样子。一个很常见的场景是AJAX请求返回400,这通常不是后台逻辑问题,而是前端传的参数名和Controller方法参数名对不上,或者参数类型不匹配,比如传了字符串给一个Integer类型的参数。

Console面板里如果看到Uncaught TypeError: xxx is not a function,大概率是JS文件引用顺序错了。比如你引用了JQuery的脚本,但后面的代码在JQuery加载之前就执行了,就会出现这种情况。还有一个小坑是重名函数,或者方法名和页面内置属性冲突。JS的调试有时候比Java还折磨人,但是只要耐心看console里的报错行数,基本都能快速定位问题。

写在最后的几点体会

把整个SSM公司企业OA管理系统完整做下来,我最大的感受是:这种项目的核心不在于某个技术有多深,而在于把一堆常规技术组合成一个自洽的业务系统。你既要懂Spring和MyBatis的衔接,又要能设计合理的数据库表,还要会写一点不难但容易出错的JSP和JS代码。任何一个环节掉链子,整个系统都跑不顺。

我特别建议你把审批模块当作重点来打磨,因为它是OA系统最有业务代表性的地方,答辩时老师问得最多的也是这种“业务状态流转”的问题。能在纸上把状态图画清楚,再把状态变化落实到数据库字段和Service逻辑里,就已经能证明你不只是在背框架,而是真的理解了怎么做企业应用。数据库设计上多花点时间,把字段格式、关联关系都敲定再动手写代码,你会发现后面写起来意外的顺畅。希望这篇内容对正在做类似项目的你有些帮助,也祝你的项目一次跑通、答辩顺利。

返回列表