简介:基于若依RuoYi前后端分离架构开发的待办事项管理系统,后端采用Spring Boot + MyBatis,前端使用Vue(ruoyi-ui),集成Quartz实现定时提醒功能。提供完整可运行代码,包含homework-admin、homework-system、homework-common等模块,配套ry_20220822.sql和quartz.sql数据库初始化脚本,支持任务创建、截止时间设定、状态标记、数据持久化及基础权限控制。附带Windows一键运行脚本(run.bat、clean.bat)和Linux启动脚本(ry.sh),适配IntelliJ IDEA、Eclipse等主流IDE,数据库兼容MySQL,可用Navicat或MySQL Workbench快速导入并启动。项目结构清晰,注释规范,开箱即用,适合计算机专业课程设计、期末大作业或Java方向毕业设计直接参考或二次开发。
1. 项目概述:这不是又一个“Todo List”,而是一套能进答辩PPT的毕设级待办系统
你是不是也经历过这样的时刻:导师说“毕设要体现工程能力”,你翻遍GitHub,看到的不是单页Vue Todo、就是Spring Boot裸写CRUD,连个登录页都要自己从零抠;好不容易凑齐前后端,一跑起来——定时提醒没动静,数据库表缺字段,权限菜单点不开,最后只能在答辩现场对着控制台报错截图硬讲“这个异常体现了我对事务传播的理解”……太真实了。这套基于若依RuoYi搭建的待办提醒系统,就是为解决这种“毕设落地最后一公里”问题而生的。它不是玩具Demo,而是真正按企业级Java项目规范组织的完整工程:后端用Spring Boot 2.6.x + MyBatis-Plus + Shiro(或可选Spring Security),前端是成熟稳定的ruoyi-ui(Vue 2.6 + Element UI),数据库脚本直接对应若依官方2022年8月版本结构(ry_20220822.sql),连Quartz调度表都给你配齐(quartz.sql)。更关键的是,它把“开箱即用”落到了实处——Windows下双击run.bat就能拉起整个服务,Linux下执行./ry.sh start同样生效,连IDE导入路径、Maven依赖冲突、MySQL时区配置这些新手最容易卡壳的环节,都在脚本和注释里埋好了钩子。它适合谁?计算机专业大三下到大四上做课程设计的同学,需要快速验证业务逻辑、展示系统架构能力;也适合Java方向毕业设计选题撞车“学生管理系统”“图书借阅系统”的同学,用“智能待办+定时提醒”这个切入点,既避开同质化,又能自然带出Quartz原理、任务状态机、权限粒度控制等技术亮点。我带过十几届毕设,最常被问的问题是:“老师,我的系统怎么显得不像‘抄的’?”答案就藏在这套项目的模块命名里——homework-admin不是随便起的,它暗示你后续可以扩展“作业提交”“教师批阅”“学生成绩关联”等教育场景;homework-system里预留的task_reminder_log表,就是为你准备的“提醒触发次数统计”“用户免打扰设置”等答辩加分项。它不教你从零造轮子,而是给你一套打磨好的底盘,让你专注把“轮子上的车身”——也就是你的业务创新点——做得扎实、讲得清楚。
2. 整体架构与模块拆解:为什么选若依?不是因为“简单”,而是因为“可控”
很多人第一反应是:“若依太重了,一个小待办系统用得着吗?”这个问题问到了根子上。我们来算一笔账:如果你自己从头搭一个Spring Boot + Vue分离项目,光是基础建设就要踩多少坑?后端要配MyBatis分页插件、Swagger文档、Redis缓存开关、日志脱敏规则;前端要搞Vue Router路由守卫、Axios请求拦截、Element UI表单校验联动、权限按钮级控制……这些加起来,至少3天起步,而且全是重复劳动。而若依的价值,恰恰在于它把这些“非业务性但必须存在”的基础设施,封装成了开箱即用的模块,并且做了深度解耦。你看它的目录结构:homework-common放的是所有模块共用的工具类、常量、异常定义、通用DTO;homework-framework封装了Shiro权限认证、Redis缓存模板、Excel导出工具;homework-system才是真正的业务核心,里面task包下有TaskEntity实体、TaskMapper接口、TaskService逻辑、TaskController接口,层次清晰得像教科书。这种结构不是为了炫技,而是为了让你在答辩时能指着代码说:“老师,这里我把任务创建的业务逻辑抽离成独立Service,配合@Transactional保证数据一致性;而定时提醒的触发逻辑,我放在了homework-framework的Quartz配置里,和业务代码完全隔离——这体现了高内聚低耦合的设计思想。”这才是毕设该有的样子。至于为什么选Shiro而不是Spring Security?实测下来,在若依这个体量的项目里,Shiro的配置更轻量,@RequiresPermissions("task:add")这种注解式权限控制,比Security的@PreAuthorize("hasAuthority('task:add')")更直观,对答辩演示更友好。当然,如果你的毕设要求必须用Security,替换成本也很低——homework-framework里的ShiroConfig类删掉,换成SecurityConfig,再调整几个Bean注入,半天就能搞定。重点在于,若依给了你一个“可替换、可演进”的基座,而不是一个锁死的黑盒。
2.1 模块职责与协作关系:一张图看懂数据怎么流
整个系统的数据流向,其实就围绕一个核心动作:用户创建任务 → 系统持久化 → Quartz扫描到期任务 → 触发提醒逻辑 → 更新任务状态/推送通知。我们拆开每个模块看它是怎么参与这个过程的:
-
homework-admin:这是前端工程打包后的产物,本质是静态资源。它通过axios调用homework-system暴露的REST API(比如/task/add、/task/list),把用户输入的任务标题、截止时间、优先级等参数传过去。关键点在于,它不处理任何业务逻辑,只负责渲染和交互。比如“设置截止时间”,前端用Element UI的el-date-picker组件,格式化成yyyy-MM-dd HH:mm:ss字符串传给后端,绝不自己计算“距离现在还有多少小时”。 -
homework-system:这是真正的业务大脑。它接收homework-admin的请求,先走Shiro权限校验(比如只有role_teacher角色才能创建高优先级任务),再调用TaskService。TaskService里有个关键方法叫createTask(TaskEntity task),它会做三件事:一是校验任务截止时间不能早于当前时间(防止创建已过期任务),二是调用taskMapper.insert(task)写入数据库,三是调用quartzJobService.addJob(task)向Quartz调度器注册一个定时任务。注意,这里不是直接执行提醒,而是“预约提醒”——就像你手机闹钟设好时间,但真正响铃是系统底层做的事。 -
homework-framework:这里藏着Quartz的“心脏”。QuartzConfig类配置了SchedulerFactoryBean,指定了线程池大小(默认10)、是否持久化到数据库(必须开启!否则服务重启提醒就丢了)。TaskQuartzJob类继承QuartzJobBean,实现了executeInternal方法——这才是真正干活的地方:它从数据库查出所有status='WAITING' AND end_time <= NOW()的任务,逐个调用taskService.remindTask(taskId)更新状态为REMINDING,并调用消息推送服务(项目里预留了MessageService.sendRemindMsg()接口,你可以对接邮件、短信或站内信)。所有这些,都通过@Autowired注入,没有硬编码。 -
homework-common:提供跨模块的“胶水”。比如Result统一返回对象,让前后端约定好code=200表示成功、code=500表示失败;Constants类里定义了任务状态枚举TASK_STATUS_WAITING="0"、TASK_STATUS_REMINDING="1",避免各处写魔法数字;ExcelUtil工具类让导出任务列表变成一行代码ExcelUtil.exportExcel(list, "待办任务", TaskEntity.class)。这些细节,恰恰是毕设答辩时评委最看重的“工程素养”。
提示:很多同学导入项目后启动报错,90%是因为没看清模块依赖关系。
homework-system的pom.xml里必须有<dependency><groupId>com.homework</groupId><artifactId>homework-framework</artifactId><version>1.0.0</version></dependency>,否则QuartzJobBean找不到。若依的Maven多模块结构,要求父pom.xml的<modules>标签里明确列出所有子模块,顺序不能错——homework-common必须在homework-framework之前,homework-framework必须在homework-system之前,否则编译会失败。
2.2 若依框架的“安全边界”:哪些能改,哪些建议别碰
若依是个成熟的框架,但它不是万能的。作为毕设使用者,你必须清楚它的“安全边界”,否则容易好心办坏事。我总结了三条铁律:
第一,绝对不要动ruoyi-ui的src/layout和src/router/index.js。这两个文件是若依前端的骨架,layout定义了顶部导航栏、侧边菜单栏、内容区域的布局,router则管理所有页面路由。你新增一个“提醒记录查询”页面,正确做法是在src/views/task下新建remindLog.vue,然后在router/index.js里加一条{ path: '/task/remindLog', component: () => import('@/views/task/remindLog.vue'), name: 'RemindLog', meta: { title: '提醒记录', icon: 'log' } }。千万别去改layout里的<el-menu>结构,否则可能导致整个菜单栏崩溃。
第二,homework-common里的BaseEntity和BaseMapper可以继承,但不要修改其内部逻辑。BaseEntity已经封装了createBy、createTime、updateBy、updateTime等审计字段,BaseMapper提供了selectPage分页方法。你新建TaskEntity时,只需extends BaseEntity,并在@TableField注解里指定fill = FieldFill.INSERT即可自动填充创建时间。如果手贱去改BaseEntity的@TableLogic逻辑,会导致所有模块的软删除失效。
第三,Quartz的qrtz_开头的11张表,必须用quartz.sql初始化,绝不能手动建表或删表。quartz.sql里不仅有表结构,还有关键索引(如idx_qrtz_j_req_recovery用于加速恢复失败任务),以及qrtz_job_details表的job_data字段是BLOB类型,用来存储序列化的JobDetail对象。我见过有同学用Navicat图形化界面建表,把job_data设成TEXT,结果Quartz启动时报java.io.StreamCorruptedException——因为序列化字节流被截断了。记住:quartz.sql是Quartz官方认证的建表脚本,照着执行就完事。
3. 核心功能实现详解:从“创建任务”到“准时提醒”的全链路
现在我们聚焦最核心的业务闭环:用户在前端点“新建任务”,填完信息点保存,几秒钟后收到提醒。这个看似简单的操作,背后涉及至少7个关键环节的精准配合。下面我带你一步步拆解,每一步都附上真实代码片段和避坑要点。
3.1 任务创建与持久化:不只是insert,还有状态机和时间校验
用户点击保存后,前端task/add.vue会收集表单数据,调用API:
// src/api/task.js
export function addTask(data) {
return request({
url: '/task/add',
method: 'post',
data: data
})
}
后端TaskController接收请求:
// homework-system/src/main/java/com/homework/system/controller/TaskController.java
@PostMapping("/add")
@RequiresPermissions("task:add")
public AjaxResult add(@RequestBody TaskEntity task) {
// 关键校验1:截止时间不能早于当前时间(防止创建已过期任务)
if (task.getEndTime().before(new Date())) {
return AjaxResult.error("截止时间不能早于当前时间");
}
// 关键校验2:任务标题不能为空,且长度不超过50字符
if (StringUtils.isEmpty(task.getTitle()) || task.getTitle().length() > 50) {
return AjaxResult.error("任务标题不能为空且长度不能超过50个字符");
}
// 执行业务逻辑
return toAjax(taskService.insertTask(task));
}
TaskService.insertTask()方法里,除了调用taskMapper.insert(task),还做了两件重要的事:
1. 状态初始化:task.setStatus(TaskStatus.WAITING.getCode());,把新任务状态设为“等待提醒”,而不是直接设为“已完成”。
2. Quartz任务注册:quartzJobService.addJob(task);,这才是提醒功能的起点。
注意:
quartzJobService.addJob(task)不是立即执行,而是向Quartz调度器提交一个“未来事件”。它会根据task.getEndTime()计算出触发时间,比如用户设的截止时间是2024-10-15 14:30:00,那么Quartz就会在这个时间点精确触发一次TaskQuartzJob.executeInternal()。这个过程完全异步,不影响用户创建任务的响应速度。
3.2 Quartz定时提醒的深度集成:不只是“每隔X秒执行”,而是“精准到秒的到期触发”
若依默认的Quartz配置是内存模式,服务重启后所有定时任务就丢失了。毕设必须用数据库持久化模式,否则答辩时评委问“服务宕机半小时,到期任务会不会漏提醒?”,你就答不上来了。homework-framework/src/main/resources/quartz.properties里关键配置如下:
# 必须开启集群模式,支持多实例部署(虽然毕设一般单机,但配置要规范)
org.quartz.jobStore.isClustered = true
# 指定数据库表前缀,必须和quartz.sql里的表名一致
org.quartz.jobStore.tablePrefix = QRTZ_
# 数据源配置,指向你的MySQL
org.quartz.dataSource.myDS.driver = com.mysql.cj.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
org.quartz.dataSource.myDS.user = root
org.quartz.dataSource.myDS.password = 123456
org.quartz.dataSource.myDS.maxConnections = 5
最关键的,是TaskQuartzJob的实现:
// homework-framework/src/main/java/com/homework/framework/quartz/job/TaskQuartzJob.java
@Component("taskQuartzJob")
public class TaskQuartzJob extends QuartzJobBean {
@Autowired
private TaskService taskService;
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
// 1. 获取触发此Job的JobDetail中携带的taskId参数
JobDataMap dataMap = context.getMergedJobDataMap();
Long taskId = dataMap.getLong("taskId");
// 2. 查询任务详情,双重校验:确保任务状态仍是WAITING,且截止时间确实已到
TaskEntity task = taskService.selectTaskById(taskId);
if (task == null || !TaskStatus.WAITING.getCode().equals(task.getStatus())
|| task.getEndTime().after(new Date())) {
// 任务已被处理或时间未到,直接退出,避免重复提醒
return;
}
// 3. 执行提醒逻辑:更新状态 + 推送消息
try {
taskService.remindTask(taskId);
// 这里可以扩展:调用邮件服务、短信网关、或写入站内信表
MessageService.sendRemindMsg(taskId, task.getTitle(), task.getEndTime());
} catch (Exception e) {
// 记录错误日志,但不抛出异常,否则Quartz会标记Job为失败
log.error("任务提醒执行失败,taskId={}", taskId, e);
}
}
}
这段代码体现了三个毕设级设计思想:
- 幂等性保障:每次执行前都查库校验状态和时间,即使Quartz因网络抖动重复触发,也不会造成多次提醒。
- 失败隔离:提醒逻辑用try-catch包裹,单个任务失败不影响其他任务执行。
- 参数传递:taskId是通过JobDataMap传入的,而不是在Job里查库获取,减少了数据库压力。
实操心得:初学者常犯的错误是,在
executeInternal里直接写taskService.updateTaskStatus(taskId, "REMINDING"),却忘了查库确认状态。结果用户手动把任务标记为“已完成”,Quartz还在后台默默执行提醒,导致状态错乱。务必记住:Quartz Job是“触发器”,不是“执行器”,真正的业务判断必须在Job里做。
3.3 前端提醒交互:不只是弹窗,而是可操作的状态反馈
后端提醒逻辑执行后,前端如何感知?这里有个精妙的设计:homework-admin并没有轮询后端接口,而是利用了若依的WebSocket长连接能力(homework-framework里已集成spring-boot-starter-websocket)。当TaskService.remindTask()更新任务状态为REMINDING时,会同步触发一个WebSocket广播:
// homework-system/src/main/java/com/homework/system/service/impl/TaskServiceImpl.java
@Override
@Transactional
public void remindTask(Long taskId) {
TaskEntity task = new TaskEntity();
task.setTaskId(taskId);
task.setStatus(TaskStatus.REMINDING.getCode());
task.setUpdateTime(new Date());
taskMapper.updateTaskStatus(task);
// 关键:广播提醒事件
webSocketServer.sendMessageToAll("REMIND_TASK:" + taskId);
}
前端task/list.vue里监听这个事件:
// src/views/task/list.vue
mounted() {
// 初始化WebSocket连接
this.initWebSocket();
},
methods: {
initWebSocket() {
// 若依封装了WebSocket工具类
this.webSocket = new WebSocket('ws://' + window.location.host + '/websocket');
this.webSocket.onmessage = (event) => {
const data = event.data;
if (data.startsWith('REMIND_TASK:')) {
const taskId = data.split(':')[1];
// 刷新当前列表,或给对应任务行添加闪烁动画
this.getList();
this.$notify.success({
title: '待办提醒',
message: `任务【${this.getTaskTitleById(taskId)}】已到期,请及时处理!`,
duration: 5000
});
}
};
}
}
这样,用户在浏览器里根本不用刷新页面,就能实时收到提醒。而且,这个提醒不是冷冰冰的弹窗,而是和具体任务绑定的——点击通知,可以直接跳转到该任务的编辑页。这种细节,能让答辩时的演示效果提升一个档次。
4. 一键启动与环境适配:从零到运行,三步到位
很多同学卡在第一步:导入IDE后,启动就报错。根源往往不是代码问题,而是环境配置没对齐。下面我把Windows和Linux下的启动流程,拆解成可复制粘贴的操作步骤,并标注每个步骤背后的原理。
4.1 Windows环境:run.bat的“黑魔法”解析
双击run.bat就能启动,它到底干了什么?我们打开文件看真相:
@echo off
echo 正在启动Homework待办系统...
echo.
:: 步骤1:切换到homework-admin目录,安装前端依赖(仅首次需要)
cd /d %~dp0\homework-admin
if not exist node_modules (
echo 正在安装前端依赖...
npm install --registry https://registry.npm.taobao.org
)
:: 步骤2:启动前端开发服务器(端口80)
start cmd /k "npm run dev"
:: 步骤3:切换到项目根目录,启动后端(端口8080)
cd /d %~dp0
echo 正在启动后端服务...
mvn clean spring-boot:run -DskipTests -pl homework-system -am
pause
看到没?run.bat其实做了三件事:
1. 前端依赖安装:检查homework-admin/node_modules是否存在,不存在就执行npm install。这里用了淘宝镜像源,避免国内网络下载慢。
2. 前端服务启动:用start cmd /k新开一个命令行窗口,运行npm run dev,启动Vue开发服务器,默认端口80(若被占用,可在homework-admin/vue.config.js里改devServer.port)。
3. 后端服务启动:回到项目根目录,用Maven命令mvn clean spring-boot:run启动homework-system模块。-pl homework-system指定只构建这个模块,-am表示同时构建它的依赖模块(如homework-framework),-DskipTests跳过测试用例加速启动。
注意:
run.bat里cd /d %~dp0是关键。%~dp0代表当前bat文件所在目录的盘符和路径,确保无论你在哪个目录双击它,都能准确定位到项目根目录。很多同学把bat文件拷到桌面运行,结果路径错乱,找不到homework-admin目录。
4.2 Linux环境:ry.sh的健壮性设计
ry.sh比run.bat更复杂,因为它要考虑生产环境的健壮性:
#!/bin/bash
# ry.sh
APP_NAME="homework-system"
JAR_PATH="./homework-system/target/homework-system.jar"
PID_FILE="/var/run/homework.pid"
start() {
echo "Starting $APP_NAME..."
# 检查JAR包是否存在
if [ ! -f "$JAR_PATH" ]; then
echo "Error: JAR file not found at $JAR_PATH"
echo "Please run 'mvn clean package -Dmaven.test.skip=true' first."
exit 1
fi
# 启动JAR,输出日志到logs/app.log
nohup java -jar -Dfile.encoding=UTF-8 "$JAR_PATH" > logs/app.log 2>&1 &
echo $! > "$PID_FILE"
echo "$APP_NAME started with PID $(cat $PID_FILE)"
}
stop() {
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
echo "Stopping $APP_NAME (PID: $PID)..."
kill -15 $PID
rm -f "$PID_FILE"
echo "$APP_NAME stopped."
else
echo "$APP_NAME is not running."
fi
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
sleep 2
start
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac
这个脚本的亮点在于:
- 前置检查:启动前校验homework-system.jar是否存在,避免java -jar报No such file错误。
- 进程守护:用nohup和&让Java进程后台运行,不受终端关闭影响。
- PID管理:把进程ID写入/var/run/homework.pid,方便stop命令精准杀进程,而不是killall java误伤其他Java应用。
- 优雅停机:kill -15发送SIGTERM信号,让Spring Boot有机会执行@PreDestroy方法,完成数据库连接池关闭、线程池清理等收尾工作。
实操心得:在Linux上首次运行,必须先执行
chmod +x ry.sh给脚本添加执行权限,否则./ry.sh start会报Permission denied。另外,logs/app.log目录要提前创建好,否则日志会写入当前目录,造成混乱。
4.3 数据库初始化:为什么必须按顺序执行两个SQL脚本?
项目里有两个关键SQL脚本:ry_20220822.sql和quartz.sql。它们的执行顺序不能颠倒,原因如下:
- ry_20220822.sql:这是若依框架的主数据库脚本,创建了sys_user(用户表)、sys_role(角色表)、sys_menu(菜单表)等基础权限表,以及本项目的homework_task(任务表)。它还插入了默认管理员账号admin/123456。
- quartz.sql:这是Quartz官方提供的调度表脚本,创建了qrtz_job_details、qrtz_triggers等11张表。这些表的JOB_NAME字段,会引用ry_20220822.sql里sys_user表的user_id作为外键(在若依的Shiro集成中,Quartz Job的job_group通常设为用户名)。
所以正确顺序是:
1. 在MySQL中新建数据库ry(字符集选utf8mb4,排序规则utf8mb4_unicode_ci)。
2. 用Navicat或命令行,先执行ry_20220822.sql,创建基础表和初始数据。
3. 再执行quartz.sql,创建调度表。
如果顺序反了,quartz.sql里的外键约束会创建失败,导致Quartz无法启动。
提示:
ry_20220822.sql里有一段INSERT INTO sys_menu的语句,插入了“待办管理”菜单。你可以在Navicat里执行完后,直接在sys_menu表里找到这条记录,把url字段改成/task/list,component改成task/list,这样前端菜单就能正确跳转到你的任务列表页了。这是毕设定制化的最小改动,效果立竿见影。
5. 常见问题与排查技巧实录:那些让我熬夜到凌晨三点的坑
在帮几十个同学部署这套系统的过程中,我整理了一份“血泪版”常见问题清单。这些问题,99%都源于对若依框架机制的不熟悉,而非代码bug。下面我按出现频率排序,给出定位方法和终极解决方案。
5.1 启动报错“Failed to configure a DataSource”: 数据源配置的三大陷阱
这是最高频的报错,错误堆栈末尾通常是Consider defining a bean of type 'javax.sql.DataSource' in your configuration.。别急着搜“DataSource配置”,先按顺序排查这三个点:
陷阱1:application.yml里数据库密码写错了
homework-system/src/main/resources/application.yml中:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456 # ← 这里!确认你的MySQL root密码真的是123456吗?
很多同学装MySQL时设了复杂密码,但忘了改这里的password。解决方案:打开MySQL命令行,执行SELECT user, host, authentication_string FROM mysql.user WHERE user='root';确认密码,然后改yml文件。
陷阱2:MySQL驱动版本不匹配
若依用的是mysql-connector-java 8.0.30,而你的MySQL是5.7版本。这时需要降级驱动。打开homework-system/pom.xml,找到:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
在<dependency>标签内,强制指定版本:
<version>5.1.47</version>
然后mvn clean compile重新编译。
陷阱3:MySQL时区没配
错误信息里如果有The server time zone value 'XXX' is unrecognized,说明MySQL服务器时区和JDBC URL里的serverTimezone=GMT%2B8不匹配。解决方案:登录MySQL,执行:
SET GLOBAL time_zone = '+8:00';
FLUSH PRIVILEGES;
或者,更彻底的方法是修改MySQL配置文件my.cnf(Windows是my.ini),在[mysqld]下添加:
default-time-zone = '+08:00'
然后重启MySQL服务。
5.2 前端空白页/404:路由和端口的“隐形战争”
打开http://localhost,页面一片空白,F12看Network全是404。这不是前端代码错了,而是前后端端口没对齐。
现象分析:homework-admin的vue.config.js里配置了代理:
devServer: {
port: 80,
proxy: {
'/profile': {
target: `http://localhost:8080`,
changeOrigin: true
},
'/task': {
target: `http://localhost:8080`,
changeOrigin: true
}
}
}
这意味着,前端请求/task/list时,会被代理到http://localhost:8080/task/list。但如果后端没启动,或者启动端口不是8080,代理就失效了。
排查步骤:
1. 打开浏览器开发者工具,切到Network标签,刷新页面。
2. 找到第一个404请求,比如http://localhost/task/list。
3. 如果URL里没有8080,说明代理没生效——检查vue.config.js是否被正确加载(重启npm run dev)。
4. 如果URL是http://localhost:8080/task/list,但状态码是ERR_CONNECTION_REFUSED,说明后端根本没起来——去命令行看homework-system的启动日志,找Tomcat started on port(s): 8080这一行。
终极方案:把前端端口改成8080,后端改成8081,彻底避开代理。修改vue.config.js的devServer.port为8080,修改application.yml的server.port为8081。这样访问http://localhost:8080就是前端,http://localhost:8081就是后端API,一目了然。
5.3 Quartz不触发提醒:时间、状态、日志的三重校验
任务创建后,到了截止时间,控制台静悄悄。别怀疑代码,按这个清单逐项核对:
| 检查项 | 方法 | 预期结果 |
|---|---|---|
| Quartz是否启动成功 | 查看后端启动日志,搜索Started Scheduler | 必须有Started Scheduler和ClusterManager: detected 1 other nodes(单机显示1个节点) |
| 任务是否注册到Quartz | 登录MySQL,查qrtz_triggers表,TRIGGER_NAME字段应包含你的任务ID | 如TRIGGER_NAME值为TASK_12345,说明注册成功 |
| 任务状态是否为WAITING | 查homework_task表,status字段必须是0 | 如果是1(REMINDING)或2(DONE),说明已被处理过 |
| 截止时间是否已到 | 查qrtz_triggers表的NEXT_FIRE_TIME字段,转换为北京时间 | 比如值为1728979200000,用在线时间戳转换工具,看是否等于你的截止时间 |
最隐蔽的坑是:qrtz_triggers.NEXT_FIRE_TIME是毫秒时间戳,但它是UTC时间,不是北京时间。若依的QuartzConfig里配置了org.quartz.jobStore.useProperties = false,强制使用数据库存储,所以必须确保MySQL服务器时区是+08:00,否则NEXT_FIRE_TIME会比北京时间晚8小时。
实操心得:我曾经为一个“提醒不触发”问题调试了6小时,最后发现是
homework-task表名写成了homework_task(下划线),而代码里写的是homeworkTask(驼峰),MyBatis-Plus默认开启map-underscore-to-camel-case,导致SQL查不到数据。解决方案:在application.yml里显式关闭驼峰转换,或统一用下划线命名表。
6. 毕设扩展与答辩技巧:如何把“待办系统”讲成技术亮点
这套系统本身是完整的,但作为毕设,你需要让它“看起来更有料”。下面分享几个低成本、高回报的扩展点,以及答辩时如何包装它们。
6.1 三个“一分钟就能加”的高价值扩展
扩展1:任务优先级可视化
- 怎么做:在TaskEntity里加priority字段(int类型,1-5),前端task/add.vue加一个el-rate评分组件。后端TaskController.list()查询时,按priority DESC, end_time ASC排序。
- 答辩话术:“我引入了任务优先级模型,通过五级评分量化紧急程度,并在列表页用不同颜色标签(红色高优、绿色低优)直观呈现,解决了用户面对大量待办时的决策焦虑。”
扩展2:提醒方式多选
- 怎么做:在homework_task表加remind_type字段(varchar(20),值为'popup','email','sms')。TaskQuartzJob.executeInternal()里,根据remind_type调用不同服务。
- 答辩话术:“我设计了可插拔的提醒策略模式,支持弹窗、邮件、短信多种通道,未来只需新增一个SmsRemindService实现类,就能无缝接入短信网关,体现了良好的开闭原则。”
扩展3:提醒历史追溯
- 怎么做:新建homework_remind_log表,字段包括id, task_id, remind_time, status(SUCCESS/FAILED)。每次remindTask()执行后,插入一条日志。
- 答辩话术:“我构建了完整的提醒审计链路,所有提醒触发都有迹可循。通过分析remind_log表,可以统计各时段提醒成功率,为系统稳定性优化提供数据支撑。”
6.2 答辩PPT的黄金三页:让评委眼前一亮
第一页:架构图(不是UML,是故事图)
别画复杂的分层架构,就画一张简单的泳道图:
- 左侧泳道:“用户视角”:画一个用户图标,旁边写“创建任务 → 设置截止时间 → 收到提醒 → 标记完成”。
- 中间泳道:“系统视角”:画一个齿轮图标,旁边写“前端Vue → 后端Spring Boot → Quartz调度 → MySQL持久化”。
- 右侧泳道:“技术亮点”:用三个图标+一句话:时钟图标+“Quartz精准到秒的到期触发”,盾牌图标+“Shiro细粒度权限控制”,数据库图标+“双SQL脚本保障数据一致性”。
这张图10秒就能让评委理解你的工作量和技术选择。
第二页:核心代码截图(只截最有说服力的)
- 截TaskQuartzJob.executeInternal()方法,用红框标出if (task.getEndTime().after(new Date())) return;这一行,旁边标注:“幂等性保障:每次触发前双重校验,杜绝重复提醒”。
- 截application.yml里spring.datasource.url那一行,红框标出serverTimezone=GMT%2B8,标注:“时区精准配置:解决跨时区部署的日期错乱问题”。
第三页:运行效果(动图胜千言)
- 录一个15秒GIF:从创建任务(填标题、选时间、点保存),到控制台打印REMIND_TASK:123,再到前端弹出通知。重点展示“时间到,立刻提醒”这个瞬间。
- 在GIF下方写一行小字:“从创建到提醒,端到端延迟<500ms,满足实时性要求”。
最后分享一个真实案例:去年有个学生,答辩时只展示了基础功能,评委问“Quartz怎么保证不丢任务?”,他答不上来。今年他加了
qrtz_fired_triggers表的查询演示,证明服务重启后,未执行的STATE='ACQUIRED'任务会自动恢复,评委当场给了最高分。记住,毕设不是比谁代码多,而是比谁思考深、谁落地稳。这套系统,就是你思考深度的载体。
简介:基于若依RuoYi前后端分离架构开发的待办事项管理系统,后端采用Spring Boot + MyBatis,前端使用Vue(ruoyi-ui),集成Quartz实现定时提醒功能。提供完整可运行代码,包含homework-admin、homework-system、homework-common等模块,配套ry_20220822.sql和quartz.sql数据库初始化脚本,支持任务创建、截止时间设定、状态标记、数据持久化及基础权限控制。附带Windows一键运行脚本(run.bat、clean.bat)和Linux启动脚本(ry.sh),适配IntelliJ IDEA、Eclipse等主流IDE,数据库兼容MySQL,可用Navicat或MySQL Workbench快速导入并启动。项目结构清晰,注释规范,开箱即用,适合计算机专业课程设计、期末大作业或Java方向毕业设计直接参考或二次开发。
2754

被折叠的 条评论
为什么被折叠?



