说实话,最早看到"SSM个人时间规划518m3"这个项目标题的时候,我第一反应是这种题会不会太普通——个人时间规划,听起来就是一套增删改查。但等我把数据库表结构和统计逻辑整个捋完之后,发现这个题目其实挺有嚼头:它既要处理常规的任务CRUD,又要解决时间维度的聚合统计,还牵涉日历视图、优先级调度这些偏业务的细节,用来演示SSM框架从后端到前端的完整协作链路,比商城类和博客类项目更聚焦、也更贴近日常开发中真正会遇到的业务形态。
这篇文章我就从拿到这样一个项目出发,把SSM框架下的个人时间规划系统从数据库设计到调试部署整个链路拆开讲一遍。如果你是Java初学者、正在准备课程设计,或者单纯想找一份SSM项目源码来跑通全流程做技术复盘,这篇内容基本可以当实现手册来用。整个项目的核心模块、表结构、关键代码位置、环境配置和常见坑都会覆盖到,不会只停留在"能跑就行"的层面。
1. SSM技术栈在"时间规划"场景下的合理性
1.1 这个系统到底要解决什么问题
个人时间规划系统的核心用户场景其实很固定:一个人登录系统后,能创建待办任务、给任务设置起止时间和优先级,按日历查看每天的时间安排,任务完成后还能回看自己的时间投入是否合理。它和个人博客、商城这类经典课设题相比,业务不算复杂,但胜在数据关系清晰、统计场景多,非常适合用来完整演示SSM三层架构怎么协同工作。
从功能清单来看,一个规范的SSM个人时间规划系统通常涵盖五个模块:
- 用户模块:注册、登录、密码不能明文存储,至少用MD5加盐处理;
- 任务模块:任务的增删改查,核心字段包括标题、描述、优先级、状态、开始时间、结束时间;
- 标签模块:给任务打标签,方便按类别筛选,比如"工作""学习""健身";
- 日历视图:按月份展示每天的任务分布,这是时间规划系统里最有辨识度的功能;
- 统计模块:按日期、优先级、状态聚合任务数据,直观呈现时间花在哪了。
这里面真正考验开发功力的不是任务CRUD,而是日历展示和统计聚合。很多初写这个题目的人会把时间字段直接存成字符串,或者在Java代码里做循环判断来统计数据,其实都是走弯路。
1.2 为什么SSM这套老技术栈今天仍然值得用
这个问题经常有人问。现在Spring Boot明显更流行,新项目基本默认Boot起步,为什么还要回头用SSM?
我的看法是,SSM的学习价值在于它把Spring、SpringMVC、MyBatis三块能力拆开来展示,每一层的职责边界非常清楚,所有配置也都是显式可见的。用Spring Boot的话,大量自动配置把细节藏起来了,初学者反而很难理解"一个请求到底是怎么从浏览器一路走到数据库,再原路返回浏览器"的完整过程。
就拿这个个人时间规划系统来说,用SSM实现,你可以清清楚楚看到:
- DispatcherServlet是怎么根据URL找到对应Controller的;
- Service层加一个
@Transactional注解之后,Spring是通过AOP机制接管事务提交和回滚的; - MyBatis的Mapper接口为什么只定义方法签名就能执行SQL,底层靠的是动态代理绑定XML里的语句;
- 数据库表字段和Java对象属性之间的下划线转驼峰映射,是在哪个配置项里生效的。
对准备面试或者打基础的人来说,这一套链路跑通了,后面看Spring Boot几乎是平滑迁移,因为Boot只是把SSM做了自动装配,底层思想还是那一套。
1.3 一次请求在SSM里是怎么流转的
说清楚这个流转过程,后面看代码才不会迷路。以"查询某天的任务列表"为例,完整链路是这样的:
浏览器发起HTTP请求,Tomcat根据web.xml里的映射规则,把请求交给SpringMVC的DispatcherServlet;DispatcherServlet拿着URL去HandlerMapping里找对应的Controller方法,找到之后通过HandlerAdapter执行该方法;Controller负责接收参数、做基本的参数校验,然后调用Service接口;Service层写好业务逻辑,如果涉及多步数据库操作就加事务,内部调用Mapper接口;Mapper接口再去执行XML里定义的SQL,把结果集通过MyBatis映射成Java对象;最后原路返回,Controller把数据塞进ModelAndView,再交给视图解析器渲染成JSP页面返回浏览器。
这条链路里每个环节都有对应的配置文件:Spring管理Service和Mapper的Bean,SpringMVC管理Controller和视图解析,MyBatis负责数据访问层。三个配置文件各管一段,边界清楚,出了问题排查起来也快。后面部署调试遇到404、500或者数据查不出来的时候,万变不离其宗,都是这条链路的某个环节断了。
2. 数据库建模:时间规划系统的表设计思路
2.1 核心表结构与字段约定
个人时间规划系统的数据库设计并不复杂,但有几个字段取舍很能体现经验。我按实际项目中比较通行的做法,给出一套完整的建表参考:
-- 用户表 CREATE TABLE `t_user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL, `password` VARCHAR(100) NOT NULL COMMENT '存放MD5加盐后的密文', `nickname` VARCHAR(50) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 任务表 CREATE TABLE `t_task` ( `id` INT NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL COMMENT '所属用户', `title` VARCHAR(100) NOT NULL COMMENT '任务标题', `description` TEXT COMMENT '详细描述', `priority` TINYINT NOT NULL DEFAULT 2 COMMENT '1高 2中 3低', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0未开始 1进行中 2已完成 3已延期', `start_time` DATETIME DEFAULT NULL COMMENT '计划开始时间', `end_time` DATETIME DEFAULT NULL COMMENT '计划结束时间', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_start` (`user_id`, `start_time`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 标签表 CREATE TABLE `t_tag` ( `id` INT NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL, `name` VARCHAR(20) NOT NULL, `color` VARCHAR(10) DEFAULT '#3b82f6', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_tag` (`user_id`, `name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 任务标签关联表 CREATE TABLE `t_task_tag` ( `task_id` INT NOT NULL, `tag_id` INT NOT NULL, PRIMARY KEY (`task_id`, `tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这套表结构里有几个设计点是很多人容易忽略的,我单独说一下。
第一,优先级和状态都用TINYINT数字表示,而不是直接存字符串"高""进行中"。原因是数字字段占空间小、索引效率高,而且前端可以通过Map做一次映射就能显示成中文,后端的统计SQL写起来也更方便,例如WHERE priority = 1比WHERE priority = '高'靠谱得多——字符串还得考虑编码和大小写问题。
第二,任务表里冗余了user_id,并且在user_id和start_time上建了联合索引。这个设计直接服务于两个高频查询:查某个用户的任务列表,以及按日期范围统计任务数量。如果不建这个索引,数据量到几千条时可能没感觉,但到几万条时,全表扫描的耗时就会变得非常明显。
第三,标签和任务是多对多关系,所以需要一张关联表。有人会把标签直接塞进任务表里用一个逗号分隔的字符串存放,查询时用LIKE来模糊匹配,这样做在数据量小的时候确实省事,但遇到"统计某个标签下有多少任务"这种需求时就非常痛苦,而且无法利用索引。既然做的是完整项目,还是老老实实建关联表更合理。
2.2 时间统计的隐藏需求
时间规划系统和普通待办清单最大的区别,就是它需要回答"我的时间到底花在了哪里"这个问题。这就意味着表结构不仅要能存任务,还要能支撑起按不同维度聚合统计的查询需求。
统计场景通常有以下几类:
- 按天统计:某一天创建了多少任务、完成了多少任务;
- 按优先级统计:高、中、低优先级的任务各占多少,完成率分别是多少;
- 按状态统计:当前有多少任务未开始、进行中、已完成、已延期;
- 按标签统计:某个标签下的任务数量和时间投入;
- 按时间段统计:最近七天、最近三十天的完成趋势。
这些统计需求如果用Java代码去实现,通常是先查出所有任务,再在内存里循环分组累加。这样写在小项目里看起来没什么问题,但其实存在两个隐患:一是随着数据量增长,内存占用和耗时都会线性上涨;二是统计逻辑分散在业务代码里,后面想调整统计口径很麻烦。
我在设计这个项目时,把大部分统计行为下沉到SQL里完成。比如统计最近七天的每日完成任务数,可以这样写:
SELECT DATE(end_time) AS day, COUNT(*) AS completed_count FROM t_task WHERE user_id = #{userId} AND status = 2 AND end_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE(end_time) ORDER BY day;用GROUP BY在数据库层面完成聚合,Java代码只需要接收查询结果并渲染,这样既简洁又高效。MySQL在GROUP BY上的表现相当不错,只要联合索引命中,这个大可放心。
还有一个容易被忽视的点:任务表的end_time在数据统计中承担了"实际完成时间"的角色。因此建议在业务规则上做一个小约束——任务状态置为已完成时,同时更新end_time为当前时间。这样统计逻辑就不需要额外维护一张操作日志表,也够用了。
2.3 MyBatis的XML映射与动态SQL
数据库表建好了,接下来就是MyBatis层面的数据访问代码。这个项目的SQL不算难,但有几个地方很适合用动态SQL来处理。
首先是任务列表的分页条件查询。用户可能按状态筛选、按优先级筛选、按时间范围筛选、按关键词搜索,这些条件组合起来,如果用字符串拼接SQL会非常痛苦,而MyBatis的<where>和<if>标签可以优雅解决:
<select id="selectTaskList" resultType="com.example.entity.Task"> SELECT * FROM t_task <where> <if test="userId != null"> AND user_id = #{userId} </if> <if test="status != null"> AND status = #{status} </if> <if test="priority != null"> AND priority = #{priority} </if> <if test="keyword != null and keyword != ''"> AND (title LIKE CONCAT('%', #{keyword}, '%') OR description LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="startDate != null"> AND start_time >= #{startDate} </if> <if test="endDate != null"> AND end_time <= #{endDate} </if> </where> ORDER BY start_time ASC, priority ASC </select>其次是批量插入任务标签关联数据。一个任务可能有多个标签,如果循环执行单条INSERT会产生很多次数据库往返,用<foreach>可以一次搞定:
<insert id="batchInsertTaskTag"> INSERT INTO t_task_tag (task_id, tag_id) VALUES <foreach collection="tagIds" item="tagId" separator=","> (#{taskId}, #{tagId}) </foreach> </insert>这里有个细节要注意:XML里小于号<会被解析成标签起始符号,所以SQL中所有小于号都要写成<,大于号写成>。如果不注意这个,配置文件一加载就会报错。
另外,MyBatis的驼峰映射记得在全局配置里打开:
<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>这样数据库字段start_time才能自动映射到Java属性startTime,否则查询结果里的时间字段全是null,排查半天都以为是SQL写错了。
3. 核心功能实现:Service层、日历视图与统计报表
3.1 Service层按业务边界拆分
有了表结构和Mapper基础,接下来是业务层的组织。我习惯在每个业务模块下先写接口,再写实现类,Controller只面向接口编程。这么做的好处是后面想替换实现或者做单元测试都方便,也更符合Spring的依赖注入使用习惯。
这个项目的Service层通常拆成四个:
UserService:注册、登录、校验用户名唯一性;TaskService:任务的增删改查、状态流转、按条件分页查询;TagService:标签的增删改查、任务与标签的关联维护;StatisticsService:聚合查询任务完成情况和时间分布。
以TaskService为例,新增任务时除了往任务表插数据,还要处理标签关联。两步操作必须放在同一个事务里,否则会出现任务创建成功但标签没绑定上的脏数据。在Spring里,只需要在实现类方法上加@Transactional注解:
@Override @Transactional public void addTask(Task task, List<Integer> tagIds) { taskMapper.insert(task); if (tagIds != null && !tagIds.isEmpty()) { taskTagMapper.batchInsertTaskTag(task.getId(), tagIds); } }这里有一个非常实际的坑:@Transactional放在private方法上不生效;同类内部通过this.xxx()调用带事务的方法也不生效,因为Spring默认通过AOP代理拦截事务,只有外部调用才能触发代理链。我第一次写SSM项目时在这个问题上卡了大半夜,最后发现事务压根没生效,这种跟头很多人都会栽一次。
3.2 Controller接口设计与参数绑定
Controller层是这个系统的门面,设计原则是参数接收要准确、返回内容要清晰。对于页面跳转和Ajax请求,我建议分开处理:常规页面跳转用ModelAndView,局部刷新和统计接口统一返回JSON。
一个典型的日历数据接口长这样:
@RequestMapping(value = "/task/calendar", method = RequestMethod.GET) @ResponseBody public Map<String, Object> getCalendarTasks( @RequestParam("year") int year, @RequestParam("month") int month, HttpSession session) { User user = (User) session.getAttribute("loginUser"); Map<String, Object> result = new HashMap<>(); List<Task> tasks = taskService.getTasksByMonth(user.getId(), year, month); result.put("code", 0); result.put("data", tasks); return result; }这里需要注意@ResponseBody配合返回对象时,SpringMVC需要Jackson依赖才能完成对象到JSON的转换。很多人部署后调用接口返回406错误,多半就是因为jackson-databind没加进pom.xml。
日期参数的接收同样容易出问题。前端传过来的日期字符串默认格式是yyyy-MM-dd,而后端Java属性是java.util.Date,如果不加注解,SpringMVC会直接报参数类型不匹配。解决办法是在接收端指定格式:
@DateTimeFormat(pattern = "yyyy-MM-dd") private Date startTime;或者在Controller方法的参数上用@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解单独指定,两种方式都行,我建议在JavaBean字段上统一配置,代码更干净。
3.3 统计报表的实现思路
统计报表是整个项目里最能体现"个人时间规划"价值的功能模块,也是我建议在写博文或答辩时重点展开的部分。它的实现可以拆成两步:后端SQL聚合,前端图表展示。
后端已经准备了一些聚合SQL,比如按状态分布统计:
SELECT SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END) AS not_started, SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS in_progress, SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) AS completed, SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) AS delayed FROM t_task WHERE user_id = #{userId};用SUM(CASE WHEN ...)这种写法可以一次查出四种状态的数量,比查四次数据库或循环累加都高效。
按标签统计时间投入,则借助关联表进行JOIN:
SELECT t.name AS tag_name, COUNT(tt.task_id) AS task_count FROM t_tag t LEFT JOIN t_task_tag tt ON t.id = tt.tag_id LEFT JOIN t_task tk ON tt.task_id = tk.id AND tk.user_id = #{userId} GROUP BY t.id ORDER BY task_count DESC;算到这里,我突然意识到《深入理解Java虚拟机》第三版确实没有提及啊。其实是我跑题了,回到正题。前端展示方面,课程设计阶段用ECharts是最省事的方案——从CDN引入一个echarts.min.js,就能画出饼图展示优先级分布、柱状图展示近七日完成趋势。服务器只提供JSON数据,前端负责渲染,职责划分得很清楚。
4. 从零到跑通:开发环境搭建与部署调试全流程
4.1 工具链与版本组合
SSM项目对环境版本的敏感度非常高,用错一个版本就可能引发各种诡异报错。这里先给出一套经过验证的稳定组合:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 最稳妥,市面上绝大多数SSM代码和教程都基于JDK8 |
| Maven | 3.6.x | 用来管理依赖和打war包 |
| Tomcat | 8.5或9.0 | 注意别用Tomcat10,包名从javax改成jakarta,老项目直接跑不起来 |
| MySQL | 5.7或8.0 | 两版都能用,但8.0的驱动类名和时区参数不一样 |
| IDEA | 2020及以上 | 社区版也够用 |
JDK版本方面要特别提醒一下,不要因为电脑里装了JDK17就直接拿来做SSM项目。老项目的web.xml可能声明的是Servlet 3.0规范,很多依赖也按JDK8编译,用17运行有时能通过,但个别老版本依赖会有反射访问方面的兼容问题。与其浪费时间解兼容性,不如老老实实装个JDK8,用IDE单独指定项目JDK版本。
Maven仓库建议把阿里云镜像配到settings.xml里,不然下载Spring依赖能急死人。如果完全没配过,直接用下面的配置替换mirror部分:
<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>4.2 数据库初始化与项目配置
拿到SSM个人时间规划系统源码后,第一个动作不是启动项目,而是先把数据库建好。一般项目都会附带database.sql或init.sql之类的脚本,直接通过Navicat或命令行执行即可。
在所有配置里,最容易出问题的是数据库连接配置。典型的jdbc.properties长这样:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/time_planner?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=123456这里有两个关键点。第一,如果你用的是MySQL 8.0,驱动类名必须是com.mysql.cj.jdbc.Driver,带不带cj决定了连接能否建立成功。第二,serverTimezone=Asia/Shanghai不能省,否则数据库连接会报时区错误,或者出现所有时间字段和本地时间相差8小时的情况。
Spring配置文件的读法一般是这样的:
<context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close"> <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>我常用DBCP2连接池,理由很简单:依赖少、配置直观。C3P0性能和它差不多,但配置项稍多,新手看到一堆maxPoolSize和acquireRetryAttempts容易发懵。
4.3 本地启动与验证
IDEA里启动SSM项目的流程,我已经走过多遍,要点归纳如下:
- 用IDEA打开项目根目录,等待Maven下载完所有依赖,这个过程首次可能需要几分钟;
- 修改
jdbc.properties里的账号密码为你的本机数据库账号; - 打开
web.xml确认DispatcherServlet配置存在,确认项目打包方式是war包; - 点击IDEA右上角的Add Configuration,选择Tomcat Server,把war包部署到Tomcat;
- 启动Tomcat,控制台看到
Server startup说明启动成功; - 浏览器访问
http://localhost:8080/项目名/,通常会自动跳转到登录页或首页。
如果能正常打开登录页,尝试注册一个新账号、创建一条带标签的任务、在日历视图中切几个月份、再到统计页面看一眼图表是否正常渲染。这套流程走完,项目在本地就算正式跑通了。
5. 调试部署阶段最容易翻车的五个问题
5.1 启动即报ClassNotFoundException
这个错误几乎人人都遇到过,原因只有一个:依赖包没进最终产物。SSM项目最常见的配置是Maven打war包,而Tomcat运行的是war包里的WEB-INF/lib目录下的jar。如果依赖只是声明在pom里而没有被正确打入war包,程序启动时就会报缺失类。
排查思路是看IDEA的External Libraries列表,或者直接解压war包检查WEB-INF/lib里有没有对应的jar。最容易缺失的是以下几个:
jackson-databind:缺少它,@ResponseBody返回JSON时直接报406;spring-webmvc:Spring MVC的入口DispatcherServlet依赖它;mysql-connector-java:缺少它,数据源初始化时根本找不到驱动类;jstl:JSP页面里的<c:forEach>标签都依赖它,没有就渲染成一大串源码。
5.2 静态资源被DispatcherServlet拦截
这是一个非常经典的坑。web.xml里把DispatcherServlet映射到/之后,默认会拦截所有请求,包括CSS、JS、图片这种静态资源。如果不处理,页面会变得光秃秃的,没有任何样式。
常规解决方式是在SpringMVC配置文件中加上静态资源放行:
<mvc:default-servlet-handler/>加上这一行后,SpringMVC会先把请求交给Servlet容器默认的Servlet处理,也就是Tomcat的DefaultServlet,由它去webapp目录下找静态文件。如果配置了多个DispatcherServlet或者路径比较复杂,也可以用<mvc:resources>精确指定静态资源目录。
5.3 所有查询结果的时间字段都是null
前文提到过MyBatis驼峰映射的事。实体类的startTime和数据库字段start_time无法自动对应,就会导致查出来的对象时间字段全是null。
检查顺序:先看MyBatis配置文件里mapUnderscoreToCamelCase有没有设为true;再看查询SQL是不是用了SELECT *,如果用SELECT start_time AS startTime这种别名写法,也能手动解决;最后检查entity类字段类型,Date类型需要正确引入java.util.Date而不是java.sql.Date。
5.4 页面中文乱码
个人时间规划系统的任务描述、标签名称都是中文,乱码问题一旦出现会直接影响观感。乱码通常是两级问题:
第一级是数据库表编码和连接编码。建表时统一用utf8mb4,连接URL加上characterEncoding=utf8参数,基本上数据库层面就不会乱码。
第二级是HTTP请求和响应编码。SpringMVC配置文件的CharacterEncodingFilter是标准解法:
<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>注意forceEncoding要设置为true,否则只会设置请求编码,不设置响应编码。我遇到过很多次"数据存进数据库是正常的,但页面上显示乱码",排查下来全是filter没配完整。
5.5 修改代码后不生效
这个问题在项目开发中非常容易让人心态爆炸。明明改了Java代码,重启Tomcat后还是原来的效果。
其实原因很简单:IDEA的热部署配置没开,或者Tomcat的Deploy方式不对。检查两处:
Build菜单下,确保Build Project Automatically开启,并且设置了Compiler的Build project automatically;- 配置Tomcat时,
On frame deactivation选择Update classes and resources。
如果这两处都设置了仍然不生效,干脆手动停掉Tomcat再重新启动。Spring的contextConfigLocation是应用启动时初始化一次的,配置文件改动后必须重启容器才能加载,不要指望它能像前端热更新那样即时生效。
6. 在原有功能基础上还能做哪些扩展
一个SSM个人时间规划系统跑通之后,它的价值不只在于交付课设或验收,更在于它是一个很好的"练手基座"。我从实际带项目的经验出发,推荐几个不改变整体架构、但能明显提升系统完成度的扩展方向。
第一,增加专注计时功能。这是时间规划天然的高频场景,用户点击"开始专注"后,后台记录开始时间和任务ID,再点击"结束专注"时保存时长,形成一条专注记录。和任务表关联后,可以统计每个任务累计投入了多少时间。这个扩展只需要一张新表加两个按钮,但对"个人时间规划"而言,却从"计划管理"升级到了"时间追踪"。
第二,接入ECharts做更丰富的可视化看板。课程设计阶段用柱状图、饼图已经够用,如果想更进一步,可以画一个热力图展示"过去一年哪些天完成的任务多、哪些天是空窗期"。这种可视化效果对答辩演示的加分效果很明显。
第三,把前端从JSP换成Vue等分离式框架。SSM只保留后端接口层,前端独立部署,整体的架构就演进成了前后端分离。如果你将来准备做企业级项目,这个改动可以帮你看清"天然的Controller职责边界在哪"。
第四,增加用户间对比或团队协作功能。个人时间规划扩展方向是多人共享计划或者由上级查看团队成员的日报。这个方向会涉及更复杂的数据权限逻辑,过程中能顺带加深对数据库查询、权限校验的认识。
我个人的经验是,扩展功能时尽量挑一两个和项目主题紧密相关的去做,比如专注计时和热力图,这两者都紧密围绕"时间"做文章,比东加一个留言板、西加一个新闻模块要顺眼得多。
最后再分享一个小技巧。整个项目调试通过后,花十分钟把项目的web.xml、Spring配置文件、MyBatis配置文件的每个标签都查一遍作用,再对着数据库表结构把每一张表的主外键关系梳理一遍,你的收获会比单纯跑起来要大得多。SSM这类项目的核心价值,说到底不在于功能有多么花哨,而在于把一条请求从浏览器到数据库的完整链路吃透。以后无论是切到Spring Boot、Spring Cloud,还是转去写其他语言的后端,这套对分层架构和请求流转的理解都是通用的。