简介:这是一套基于SSM架构的员工管理系统完整项目,适合Java Web初学者、毕业设计或课程实训参考。系统覆盖员工管理、薪酬管理、用户管理、通知管理、文件管理等核心模块,并区分超级管理员、普通管理员、临时管理员三类权限,可用于理解MVC分层、SSM整合及权限控制思路。资源共447个文件,约93.17MB,包含Java源码、JSP页面、XML配置、SQL数据库脚本及配套文档;源码与配置可直接导入IDEA2019配合Tomcat和MySQL运行,目录结构清晰,便于按模块阅读与二次开发。作者还提供了一份Markdown说明文件,帮助快速理清项目结构与启动步骤。目前已有413人学习下载,适合需要一套可运行、可讲解、可改造的员工管理项目作为练习或答辩支撑的开发者。
1. 基于SSM架构的员工管理系统:为什么今天还值得照着做
一个员工管理系统,要做的事不外乎登录、分页查员工、增删改查,但很多新手把代码写完后,前端页面却不知道跟后端怎么对上。SSM架构就是Spring、SpringMVC、MyBatis三个框架的合称:Spring管对象和事务,SpringMVC收HTTP请求,MyBatis把SQL和Java方法绑在一起。它是Spring Boot流行前Java Web的主力,至今仍是理解三层架构最好的教材,也是很多课设、毕设和小型内部系统在用的方案。适合想弄懂Java Web前后端交互全过程的初学者,也适合要交付一个能跑、能演示、有配套文档和SQL脚本的完整项目的从业者。这篇笔记按搭建顺序来写,从骨架到CRUD,再到坑最集中的联调阶段。
2. 搭SSM员工管理系统的骨架:项目结构、依赖与最小可运行配置
2.1 为什么选SSM而不直接上Spring Boot:配置即教材
一个员工管理系统在功能上其实很小:员工表、部门表、账号表、登录拦截、增删改查。用Spring Boot做,三十分钟就能把一个空壳跑起来,用MyBatis-Plus连单表CRUD都可以少写SQL。可问题是,当系统在Tomcat里报了404、或者SQL参数没传进去,Spring Boot的自动配置会把真正的原因挡在"黑匣子"里,新手排查时不知道该看哪个配置。SSM恰恰相反,每一层都是显式的。DispatcherServlet在哪、Spring容器加载哪个XML、Mapper接口怎么和SQL绑定,都在配置文件里摆着。对这些配置理解得越透,后面换成Spring Boot或其他前后端分离项目,也越快。
另一点是这类"员工管理系统"最常见的交付场景:课设、毕设、企业内部的小型人事模块。评审或甲方要的不是多新的技术栈,而是你讲得清楚请求怎么进来、数据怎么落库。SSM加JSP或HTML加Tomcat部署,正好满足一套完整的演示链路。要是我自己接这类小项目,也会先问一句:用户后续会不会加复杂的权限、工作流?如果不会,SSM足够;如果会,再考虑Spring Boot加Vue前后端分离也不迟。
2.2 目录结构与Maven依赖:分清楚controller、service、mapper三层
一个工程拿到手,先看包结构。常见的SSM员工管理系统会按controller、service、mapper、entity、interceptor这样分包:
src/main/java/com/company/ems/ ├── controller/ # 接收HTTP请求,做参数绑定和简单校验 ├── service/ # 业务逻辑,事务在这一层声明 │ └── impl/ ├── mapper/ # MyBatis的Mapper接口,对应XML里的SQL ├── entity/ # 对应数据库表的实体类 ├── interceptor/ # 登录拦截、权限拦截 └── config/ # 如果不用XML,也可以放注解配置 src/main/resources/ ├── mapper/ # EmployeeMapper.xml、DepartmentMapper.xml ├── jdbc.properties ├── spring-mvc.xml ├── applicationContext.xml └── mybatis-config.xml src/main/webapp/ ├── WEB-INF/ │ ├── web.xml │ └── views/ # JSP页面,前后端没分离时放这里 ├── static/ # css/js/images └── index.jspentity还有个别名叫pojo或model,叫法不同,作用相同。controller层尽量只做参数接收和视图转发,不做SQL拼装;service层承担事务和业务规则;mapper层只负责与数据库对话。这样分工的好处是,出问题时顺着请求链路逐层定位,不用在Servlet里找业务代码。
Maven依赖我一般这样给,去掉了和具体版本强绑定的写法,只看必需项:
<dependencies> <!-- Spring MVC 与 Spring 核心 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.x</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.x</version> </dependency> <!-- MyBatis 与 Spring 整合 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.x</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.x</version> </dependency> <!-- 数据库驱动与连接池。注意 mysql 8.x 驱动类名和 5.x 不同 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.x</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.x</version> </dependency> <!-- 分页插件 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.3.x</version> </dependency> <!-- JSON 转换,前后端联调时靠它 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.x</version> </dependency> <!-- Servlet API 和 JSTL,打包成 war 时需要 provided 范围 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>这里有两个参数要特别留意。第一个是MySQL驱动类名:mysql-connector-java 5.x 用 com.mysql.jdbc.Driver,8.x 用 com.mysql.cj.jdbc.Driver,写错启动时立刻报 ClassNotFoundException。第二个是servlet-api的scope,必须配provided,否则和Tomcat自带的Servlet实现冲突,部署后会出现奇怪的方法签名错误。
提示:真实落地时把版本号里的 x 替换成你本机仓库里能拉到的最新小版本。这里不写死,是因为不同Spring版本和MyBatis版本之间有兼容边界,照抄固定版本反而容易在小版本上翻车。
2.3 让Tomcat能跑起来的三份配置:web.xml、Spring与SpringMVC的加载顺序
配置是SSM里最容易翻车的地方。顺序上,web.xml最先被Tomcat读取,Spring根容器和SpringMVC子容器都在这里装配:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- 编码过滤器:放在所有过滤器最前面,解决POST中文乱码 --> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- Spring 根容器 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- SpringMVC 前端控制器:拦截所有请求 --> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>加载顺序是:Tomcat先创建ContextLoaderListener,加载applicationContext.xml里的数据源、service、mapper这些根容器bean;随后创建DispatcherServlet,加载spring-mvc.xml里的controller、视图解析器、静态资源映射。父子容器的规则是子容器能看见父容器的bean,反向不行。所以事务拦截器写在applicationContext.xml里,controller扫描写在spring-mvc.xml里,是SSM的标准切法。
spring-mvc.xml里常翻车的是静态资源。把DispatcherServlet映射到/之后,css和js默认会被它拦截,前端页面就变得光秃秃。常见做法是单独放一行资源映射:
<context:component-scan base-package="com.company.ems.controller"/> <mvc:annotation-driven/> <!-- 放行静态资源,否则css/js全部404 --> <mvc:resources mapping="/static/**" location="/static/"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>InternalResourceViewResolver的prefix和suffix要按实际目录改,这里写成/WEB-INF/views/下放JSP。很多人把JSP放在webapp根目录也能跑,但放在WEB-INF里更安全,用户不能直接通过URL访问到页面源码。
applicationContext.xml承载数据源和事务,是最容易看出一个人功底的文件:
<context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.company.ems.entity"/> <property name="plugins"> <array> <bean class="com.github.pagehelper.PageInterceptor"> <property name="properties"> <value>helperDialect=mysql</value> </property> </bean> </array> </property> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.company.ems.mapper"/> </bean> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="txManager"/>mapperLocations、typeAliasesPackage、basePackage这三个属性只要有一个写错,启动时不会立刻报错,等到调用Mapper方法才提示找不到SQL或不能创建代理。另外事务管理器配好后,记得在service实现类上用@Transactional,不然一个方法里先插员工后插部门,第二句失败时第一句不会回滚。
3. 员工模块核心功能的实现路径:Controller、Service、Mapper各管什么
3.1 员工表和实体类:字段类型决定后面的所有参数
先建表再写代码。员工管理系统的SQL脚本一般包含三张表:员工表、部门表、账号表。账号表承载登录,员工表和部门表做外键关联:
CREATE TABLE `dept` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `employee` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `gender` tinyint NOT NULL COMMENT '0女 1男', `birthday` date DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `dept_id` int NOT NULL, `entry_date` date DEFAULT NULL, `status` tinyint DEFAULT '1' COMMENT '1在职 0离职', PRIMARY KEY (`id`), KEY `idx_dept` (`dept_id`), CONSTRAINT `fk_dept` FOREIGN KEY (`dept_id`) REFERENCES `dept` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字段注释为什么建议全写上?因为配套文档和SQL脚本是要给别人看的。离职状态用status,不用delete_flag走逻辑删除,这个小系统里尽量少做一版复杂设计;性别用tinyint存0/1,比字符串省空间也更好扩展,但前端显示时要记得做转换。
对应的实体类这样写:
public class Employee { private Integer id; private String name; private Integer gender; // 0女 1男,避免和boolean的语义混淆 @JsonFormat(pattern = "yyyy-MM-dd") private Date birthday; // 注意日期序列化,后面联调会用到 private String phone; private Integer deptId; @JsonFormat(pattern = "yyyy-MM-dd") private Date entryDate; private Integer status; private String deptName; // 冗余字段,用于列表显示部门名称 // getter/setter 省略 }注意表里的是dept_id,Java里是deptId,MyBatis默认驼峰映射。如果连接参数里没有配置mapUnderscoreToCamelCase=true,查出来dept_id赋值不到deptId上,值是null。这个开关一般在mybatis-config.xml里打开,属于新手最不容易发现的隐藏坑。
3.2 分页查询的Service层设计:为什么用PageHelper而不是手写limit
很多项目里员工表也就几百上千行,手写limit分页完全够用。可一旦排序条件、筛选条件一多,每个方法都要重复算total、写count语句,容易在SQL里拼出注入点。PageHelper的做法是在执行查询之前,用拦截器把下一条SQL改写成带limit的查询,并自动补一条count。SSM里集成的参数上一章已经见过:
<property name="plugins"> <array> <bean class="com.github.pagehelper.PageInterceptor"> <property name="properties"> <value>helperDialect=mysql</value> </property> </bean> </array> </property>Service接口与实现:
public interface EmployeeService { PageInfo<Employee> page(int pageNum, int pageSize, String keyword); Employee getById(Integer id); int add(Employee employee); int update(Employee employee); } @Service public class EmployeeServiceImpl implements EmployeeService { @Autowired private EmployeeMapper employeeMapper; @Override @Transactional(readOnly = true) public PageInfo<Employee> page(int pageNum, int pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); // 这之后的第一条查询会被PageInterceptor改写 List<Employee> list = employeeMapper.selectByCondition(keyword); return new PageInfo<>(list); } }两个参数要说明白。pageNum是第几页,从1开始,不是0。pageSize是每页条数,前端下拉里常见10、20、50。PageHelper有个坑:startPage后面必须紧跟第一条Mapper查询,中间不能插别的SQL,否则拦截器会把无关查询改写成分页,导致数据错乱。还有一种常见误用是页面传入pageSize=0,PageHelper会把0当成不分页,然后返回全部数据,接口响应时间突然变长,排查时先看前端有没有把初始化值写对。
PageInfo里封装了total、pages、list等属性,Controller拿到后直接转JSON返回给前端。这样前端分页组件不需要自己做total计算。
3.3 新增与更新:前端字段名与后端对象的对齐
CRUD里的C和U,最常见的翻车点是前端提交的字段名和后端实体类对不上。比如前端表单写deptId,后端对象叫dept_id,SpringMVC在参数绑定时找不到对应setter,员工新增后部门永远是null。下面是一个标准的新增接口写法:
@Controller @RequestMapping("/employee") public class EmployeeController { @Autowired private EmployeeService employeeService; @PostMapping("/add") @ResponseBody public Result add(@RequestBody Employee employee) { // 先做简单校验,name为空和deptId非法直接拒绝 if (employee.getName() == null || employee.getName().trim().isEmpty()) { return Result.error("姓名不能为空"); } if (employee.getDeptId() == null) { return Result.error("必须选择部门"); } int rows = employeeService.add(employee); return rows > 0 ? Result.success() : Result.error("新增失败"); } }用@RequestBody时,前端发的是JSON,字段名必须与Java属性名一致。如果项目里习惯用表单提交,就把@RequestBody去掉,改成form表单的key-value,实体类同样能接收。两种方式不要混着用,前后端联调时最常出现的415错误,多半就是后端在等JSON、前端却用表单提交。
前端对应的提交片段:
fetch('/ems/employee/add', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ name: document.querySelector('#name').value, gender: parseInt(document.querySelector('#gender').value), birthday: document.querySelector('#birthday').value, phone: document.querySelector('#phone').value, deptId: parseInt(document.querySelector('#deptId').value) }) })birthday是date类型,表单输入"2024-05-01"这种字符串,SpringMVC需要配置日期转换器,否则抛400。常见做法有两种:实体字段加@DateTimeFormat(pattern = "yyyy-MM-dd"),或者在Controller里加@InitBinder。前者更省事,但注意它与@JsonFormat的区别,一个是接收参数时生效,一个是返回JSON时生效,两个都不要落下。
更新操作和新增的区别在于必须传id。更新时,前端一般把整行数据回显到表单,改完再提交。后端update方法里要判断id是否为空,不然MyBatis的update语句会把整张表都更新掉。SQL里习惯写成带条件:
<update id="updateById"> update employee <set> <if test="name != null">name = #{name},</if> <if test="phone != null">phone = #{phone},</if> <if test="deptId != null">dept_id = #{deptId},</if> </set> where id = #{id} </update>动态set的好处是前端只传改动过的字段也能更新,其他字段保持原值。但有一个注意点:如果前端把没改的字段也传成空字符串,<if test="phone != null">仍然成立,空字符串会被写进数据库。所以需要在前端提交前把空字符串转成null,或者在后端单独做一次空串判断。这种细节写进配套文档里,后面接手的人会感谢你。
4. 前后端联调与SQL脚本:五个必踩的坑和排查手法
4.1 现象:登录后刷新就丢session,页面被拦回登录页
点击登录跳转到主页,一切正常。按F5刷新,马上被拦回登录页。用浏览器看cookie,JSESSIONID还在,但每次刷新值都在变。
原因:前后端不在同一个端口或同一个上下文路径时,Cookie的path不匹配。SSM项目如果用JSP渲染,页面地址是/ems/index.jsp,请求接口走/ems/employee/list,通常没问题。但一旦把前端页面独立成另一个端口,比如前端8081、后端8080,浏览器不会把后端的JSESSIONID自动带给前端发起的跨域请求。
解决:先分清楚部署形态。SSM最省事的做法是前后端不分离,JSP直接放WEB-INF,所有请求同源,Cookie自动生效。真要做前后端分离,就统一用Token方案,而不是依赖Session。若坚持用Session,后端接口要配置允许跨域携带凭据,前端fetch也要加credentials: 'include'。这三个条件缺一个,登录状态必丢。
4.2 现象:接口返回的员工生日变成一串数字
列表接口返回的birthday字段是1700000000000这样的毫秒时间戳,前端格式化后显示成"2033"年或直接空白。
原因:Jackson默认把java.util.Date序列化成时间戳。实体里只有@DateTimeFormat,没有@JsonFormat,导致接收参数时正常,返回JSON时失真。
解决:最省事是在实体日期字段上加@JsonFormat(pattern = "yyyy-MM-dd")。要是全项目日期格式统一,就在spring-mvc.xml里配置一个全局的ObjectMapper:
<mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="objectMapper"> <bean class="com.fasterxml.jackson.databind.ObjectMapper"> <property name="dateFormat"> <bean class="java.text.SimpleDateFormat"> <constructor-arg value="yyyy-MM-dd HH:mm:ss"/> </bean> </property> </bean> </property> </bean> </mvc:message-converters> </mvc:annotation-driven>全局配置和字段注解二选一即可,两个都配时以字段注解优先。
4.3 现象:分页查询慢SQL与SQL注入,索引失效比慢更危险
员工表加了几条筛选条件后,查询从几十毫秒变成几百毫秒。数据量其实只有几千行,但explain一看,type是ALL,整表扫描。
原因:最典型的两种。一是对索引列用了函数,比如where year(entry_date) = 2024,索引失效。二是前端传了排序字段,后端直接用${order}拼进SQL,排序字段没法用#{}预编译,同时也成了SQL注入点。
解决:日期范围查询改成entry_date >= ? and entry_date < ?的形式,让索引生效。排序字段采用白名单校验:
String[] allowed = {"id", "name", "entry_date", "dept_id"}; if (!Arrays.asList(allowed).contains(sortField)) { sortField = "id"; // 不在白名单里就回退默认排序 }这比任何参数过滤都直观。先看执行计划:
EXPLAIN SELECT * FROM employee WHERE name LIKE '%张三%'; -- 如果 type=ALL,说明这条查询在走全表扫描explain看到possible_keys有值但key为null,基本就是where条件写法破坏了索引。另外,MyBatis里能用#{}的地方不要用${},尤其在like模糊匹配时,拼接"%"要放在参数里:
<if test="keyword != null and keyword != ''"> and (name like concat('%', #{keyword}, '%') or phone like concat('%', #{keyword}, '%')) </if>4.4 现象:Tomcat部署前后端分离项目时页面404
本地IDE里直接运行Tomcat一切正常,打成war包丢进webapps后,访问首页404,或者页面出来了但css/js全是404。
原因:两件事最容易出问题。第一,期望的访问路径是/ems,但war包名不叫ems.war,Tomcat解压后的上下文路径和包名不一致。第二,页面里的静态资源用了绝对路径/src,部署到/ems下就解析到根目录去了。
解决:war包直接命名为ems.war,或者启动时指定上下文路径。页面里的资源链接统一用${pageContext.request.contextPath}拼前缀:
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/main.css">如果是前后端分离,前端是一个独立静态项目,部署时把它放到Tomcat的webapps/ems目录下,后端API也部署在同一Tomcat里,按/ems/api/**约定路径,就不会有跨域和404双重问题。这是tomcat部署前后端分离项目最常见、也最不容易出幺蛾子的做法。
4.5 现象:SQL脚本导入中文乱码或字段对不上
把配套的sql文件在Navicat或命令行里source导入,表建好了,但中文全是问号;要么导入时报错字段太长、外键失败。
原因:SQL脚本文件本身的编码和数据库连接编码不一致。最常见的是.sql文件是UTF-8,但连接参数没带characterEncoding=utf8,导入时被当成latin1处理;字段长度问题则多是utf8mb4下varchar(20)存emoji或长部门名,超出长度。
解决:数据库连接串统一带上参数:
jdbc.url=jdbc:mysql://localhost:3306/ems?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false建表SQL文件第一行加上SET NAMES utf8mb4;,导入后用SELECT name, HEX(name) FROM employee查一下,中文正常就说明编码链路通了。外键导入失败时,先检查两张表的字符集是否一致,再检查外键字段类型是否完全一样,int和bigint即使值相等,建外键也会被MySQL拒掉。这些写完记得同步到配套文档,因为换一台机器部署时会原样踩一遍。
5. 部署到Tomcat的验证清单:一个能拿去答辩或交接的收尾套路
5.1 打包命令与部署路径
到项目根目录执行mvn clean package,确保target下产出ems.war。把war包复制到Tomcat的webapps目录,启动bin/startup.sh或startup.bat。注意检查CATALINA_HOME配置,Windows上很多人从这里开始翻车:环境变量配了系统级JRE却找不到catalina。
验证清单可以按这个顺序过一遍:
| 检查项 | 验证动作 | 预期结果 |
|---|---|---|
| 启动日志 | 看catalina.out | 无ClassNotFoundException、无端口占用 |
| 上下文路径 | 访问/ems/ | 跳转到登录页 |
| 登录会话 | 登录后刷新 | 仍保持登录状态 |
| CRUD | 新增、修改、删除各一次 | 数据库对应数据变化 |
| 分页 | 第二页、每页10条 | total正确、无重复数据 |
| 日期字段 | 列表与详情接口 | 返回yyyy-MM-dd |
| 注入测试 | 搜索框输入' or 1=1 | 无报错、无全量返回 |
| 乱码 | 新增一条中文员工 | 库里显示正常 |
5.2 交付前的一小时
我一般会额外做两件事。把数据库导出一份干净的sql,里面只留初始化数据和少量演示数据,别把测试期间产生的垃圾数据一起交出去。再写一个README,把Tomcat版本、JDK版本、数据库连接配置、默认账号密码、部署步骤这五项写全。配套文档不一定要多长,但这五项缺一不可,因为接手的同事或评委老师大概率不是写这套代码的人。这个小习惯帮我省了无数次"怎么连不上库""默认密码是什么"的重复解释。希望帮到你。
本文还有配套的精品资源,点击获取