做毕业设计选“个人任务管理系统APP”这类题目时,很多同学一开始会觉得简单:不就是一个TodoList加个数据库,再加个手机页面吗?等真正动手才发现,任务管理的业务逻辑远不止“增删改查”。状态怎么流转、循环任务怎么处理、提醒怎么在APP被杀掉之后还能触发、前后端数据怎么同步,每个点都能写出一整章。这篇文章就把我从选题到答辩踩过的坑、验证过的方案完整梳理一遍,给正在做这个题目的同学一条能直接落地的路线。
1. 毕业设计的需求边界:别把任务管理做成记账本
1.1 为什么这个选题看似简单却容易跑偏
任务管理系统的核心是“任务”,但“任务”这个词在不同人眼里完全不是一回事。我见过不少同学的初版设计,把任务管理系统做成了待办事项列表加番茄钟,再加个统计分析图表,最后连“每日打卡”和“重复提醒”都混在一起,答辩时被老师一问“你这个任务和日历事件有什么区别”就卡住了。
问题的根源在于需求边界没划清楚。个人任务管理系统的定位应该是“以任务为核心对象,围绕任务的创建、分配、执行、跟踪、完成进行全生命周期管理”。这不是日历提醒,不是项目协作平台,也不是习惯打卡工具。定下这个边界,你的功能清单就不会跑偏。
1.2 用户角色和核心功能清单
个人任务管理系统虽然叫“个人”,但按毕业设计的完整度要求,最好设计成可扩展的多用户结构。这样做不是给自己加工作量,而是为了让系统架构更有说服力,也方便在论文里写“用户权限控制”这一章。
功能模块拆成四大块:
- 用户模块:注册、登录、个人信息维护。多用户场景下需要区分用户隔离,不能出现你看到别人任务的低级错误。
- 任务模块:任务CRUD、任务分类(工作/学习/生活)、优先级设置、截止时间、任务状态(待执行/进行中/已完成/已取消)、任务备注、循环任务设置。
- 统计与提醒模块:任务完成率统计、每日待办提醒、截止日期快到了的预警提醒。
- 设置模块:提醒开关、主题切换、数据导出。
这里有一个关键决策:要不要做循环任务?我的建议是必须做,但只做最基础的“每天/每周/每月”三种周期。理由是循环任务牵扯到状态判断的复杂性,比如“这个周期的任务没完成,下个周期还继续吗?”,这正好是你论文里能展示逻辑能力的点,也是答辩时能深度展开的素材。
1.3 从功能反推技术点:哪些地方值得写进论文
毕业设计不仅要“做出来”,还要“讲清楚”。把功能反推回技术点,你就能明确哪些地方需要重点设计:
| 功能需求 | 对应技术点 | 答辩时怎么讲 |
|---|---|---|
| 多用户数据隔离 | 数据库外键关联、Token鉴权 | 讲清楚任务表如何关联用户表 |
| 任务状态流转 | 状态机设计 | 画状态流转图,讲非法状态怎么拦截 |
| 循环任务生成 | 时间计算逻辑 | 用代码片段讲nextTriggerTime的计算 |
| 提醒推送 | 前台定时器 vs 系统通知 | 对比两种方案的优缺点 |
| 分页加载 | SQL的limit与参数绑定 | 说明大数据量下的性能考虑 |
这样规划下来,你的系统虽然规模不大,但该有的技术深度全都有了。接下来才是选技术栈。
2. 技术栈选型:Java生态里哪些组合能做出差异化
2.1 两条主流路线的取舍逻辑
第一个要拍板的问题是架构。个人任务管理系统有两条路线可选:
- 纯Android本地方案:SQLite数据库直接在手机端,所有逻辑都在APP里。
- 前后端分离方案:Android做客户端,用Spring Boot提供REST接口,数据存在MySQL。
如果你的毕设题目只写了“移动应用设计与实现”,没有强制要求服务端,很多同学就会图省事选纯本地方案。但我这里直接给结论:有时间的同学,务必选前后端分离。理由有三点,都很现实:
第一,纯本地方案意味着你的Java代码主要集中在Android端的Activity和Adapter里,论文里能写的东西很薄,技术深度撑不起三章内容。第二,答辩现场一旦有老师问“你的数据怎么备份?”或“换个手机数据怎么办?”,纯本地方案很难给出有说服力的回答。第三,前后端分离让你能同时展示Spring Boot、MyBatis、RESTful API、Android网络编程等多个技术栈,相当于用一套系统适配了多门课程的知识点。
2.2 后端技术选型:Spring Boot 2.x + MyBatis + MySQL
后端我用的组合是Spring Boot 2.7 + MyBatis + MySQL 8.0。这套组合在Java领域非常经典,资料多,遇到问题也好搜解决方案。Spring Boot 3.x虽然已经发布,但毕业设计没必要追新,2.7版本稳定、教程多、和你用的Android端JDK版本兼容性更好。
MyBatis和JPA的选择上,我更推荐MyBatis。原因很实际:MyBatis的SQL是手写的,对任务管理这种需要复杂查询(按状态筛选、按日期范围统计、分页)的场景控制力更强,而且毕业后找工作面试时MyBatis被问到的概率远大于JPA。
连数据库的配置,直接在application.yml里写就行:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/task_app?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.task.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置一定要开,否则数据库的task_name映射不到Java属性taskName上,每个字段都要写resultMap,工作量瞬间翻倍。
2.3 Android端架构:MVP比MVVM更适合这种小项目
Android端的架构选择,很多教程推荐MVVM,但我的经验是毕业设计这类中等偏小的项目,MVP反而更好落地。
MVVM的DataBinding或ViewModel和Lifecycle库版本一多,经常出现各种兼容问题,排查起来很费时间。而MVP的思路很直白:界面操作交给Activity/Fragment(View层),业务逻辑全扔给Presenter,数据模型走Model层。答辩的时候讲MVP也非常好讲,画一张单向依赖的图,老师一眼就能看懂你的分层设计。
如果你不想自己维护Presenter接口,也可以简化成“Activity负责UI + Service/Manager类负责业务 + Retrofit负责网络”的轻架构。核心原则只有一个:Activity里不要出现SQL语句、不要写线程池直接访问网络,代码全部分层,这不只是为了架构而架构,是避免后期增加功能时改一处崩三处。
3. 数据库与后端接口设计:把任务对象变成能跑的API
3.1 表结构设计:六张表搞定全部数据需求
任务管理系统的表结构不需要太复杂,但我见过很多人把任务表设计成“万能表”,一个表里堆了二十多个字段,最后连自己也记不清哪些字段有用。我最终落地的表结构分六张,每张都有明确职责:
用户表(t_user)
CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), created_at DATETIME DEFAULT CURRENT_TIMESTAMP );密码用BCrypt加密存储,不要明文。Spring Security里的BCryptPasswordEncoder可以直接用,比MD5安全得多,答辩时这也算一个加分点。
任务表(t_task)
CREATE TABLE t_task ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, title VARCHAR(200) NOT NULL, description TEXT, category VARCHAR(50), priority TINYINT DEFAULT 1 COMMENT '1-低 2-中 3-高', status TINYINT DEFAULT 0 COMMENT '0-待执行 1-进行中 2-已完成 3-已取消', deadline DATETIME, is_cycle TINYINT DEFAULT 0, cycle_type VARCHAR(10) COMMENT 'DAILY/WEEKLY/MONTHLY', remind_time DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES t_user(id) );这个表里有两个非常容易踩坑的点。第一个是updated_at的自动更新机制,MySQL 8.0支持ON UPDATE CURRENT_TIMESTAMP,但这个字段依赖数据库时间,如果你的服务器和客户端不在同一个时区,时间就会差八小时。第二个是deadline和remind_time的区分,很多人只保留一个截止时间,导致提醒逻辑做不准确——截止时间是任务的业务属性,提醒时间是任务的通知属性,两者必须分开。
另外,为什么任务表要冗余一个category字符串字段,而不是新建一张分类表?因为个人任务管理的分类非常固定(工作/学习/生活),没有太多动态扩展需求,用字符串更省事。如果你觉得这样不够“规范化”,可以建一张分类表,但需要权衡额外的联表查询是否值得。
循环任务记录表(t_task_cycle)
CREATE TABLE t_task_cycle ( id INT PRIMARY KEY AUTO_INCREMENT, task_id INT NOT NULL, cycle_start DATETIME NOT NULL, cycle_end DATETIME, next_trigger_time DATETIME NOT NULL, FOREIGN KEY (task_id) REFERENCES t_task(id) );这张表专门记录循环任务的触发情况。核心逻辑在next_trigger_time,每个周期完成后更新这个字段。如果用cron表达式做循环,反而会增加复杂度。我采用的是固定周期+触发时间计算的方式,代码逻辑直观,答辩也容易讲。
3.2 REST接口约定:不只是CRUD,要能经得起追问
接口设计遵循REST风格,统一返回结构。我定义了一个Result对象:
public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.message = "success"; result.data = data; return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.code = code; result.message = message; return result; } }核心接口清单:
| 接口 | 方法 | 作用 |
|---|---|---|
| /api/user/register | POST | 用户注册 |
| /api/user/login | POST | 登录并返回Token |
| /api/task/list | GET | 分页获取当前用户任务列表 |
| /api/task/{id} | GET | 获取任务详情 |
| /api/task/add | POST | 新增任务 |
| /api/task/update | PUT | 更新任务 |
| /api/task/{id}/status | PUT | 更新任务状态 |
| /api/task/delete | DELETE | 删除任务 |
| /api/task/stats | GET | 统计完成率、每状态任务数 |
在这里我要强调一个细节:不要把“更新任务”和“更新任务状态”合并成一个接口。任务状态流转有独立的业务逻辑(比如已完成的任务不能直接回到待执行),单独拆出来,后端就能在接口内部做状态合法性校验。以后加需求,比如“进行中的任务不允许取消”,直接改这个接口就行,不会影响其他字段的更新。
分页接口也不要只返回一个任务列表,应该返回一个包含总条数、总页数、当前页数据的对象:
public class PageResult<T> { private Long total; private Integer pages; private Integer current; private List<T> records; }这样Android端做分页加载时,根据total判断还有没有更多数据,比单纯看records.size()是不是等于每页条数要可靠得多。
3.3 后端核心代码落地:状态流转与循环触发
任务状态流转是后端最重要的业务逻辑。我用了一个简单的状态校验方法:
private static final Map<Integer, Set<Integer>> ALLOWED_TRANSITIONS = new HashMap<>(); static { ALLOWED_TRANSITIONS.put(0, new HashSet<>(Arrays.asList(1, 3))); // 待执行 -> 进行中/已取消 ALLOWED_TRANSITIONS.put(1, new HashSet<>(Arrays.asList(0, 2, 3))); // 进行中 -> 待执行/已完成/已取消 ALLOWED_TRANSITIONS.put(2, new HashSet<>()); // 已完成 -> 不可再流转 ALLOWED_TRANSITIONS.put(3, new HashSet<>(Arrays.asList(0))); // 已取消 -> 待执行 }这种用状态机表驱动的方式,比if-else判断更清晰。新来的同学看不懂你的代码?没关系,你论文里画一张状态流转图,所有人就明白了。
循环任务的触发逻辑,是定期扫描t_task_cycle表里next_trigger_time小于当前时间的任务,把任务重新生成一条新的(状态重置为待执行),然后更新下一次触发时间:
public void checkCycleTasks() { List<TaskCycle> dueCycles = taskCycleMapper.selectDueCycles(new Date()); for (TaskCycle cycle : dueCycles) { Task task = taskMapper.findById(cycle.getTaskId()); Task newTask = cloneTask(task); taskMapper.insert(newTask); Date nextTrigger = calculateNextTrigger(cycle.getNextTriggerTime(), task.getCycleType()); cycle.setNextTriggerTime(nextTrigger); taskCycleMapper.update(cycle); } }calculateNextTrigger按周期类型加不同时间:DAILY加一天,WEEKLY加七天,MONTHLY加一个月。注意月份加减要用Calendar而不是自己手算,跨月、闰年的坑你不想踩第二次。
4. Android端从0到1:五个核心模块的实现细节
4.1 登录与Token持久化:别用SharedPreferences硬扛
Android端登录后拿到的Token,怎么保存是很多人的第一个坑。最直接的方式是用SharedPreferences存字符串,但这会有一个问题:Token过期之后,APP每次启动都得重新登录,非常影响体验。
更优雅的方案是Token + 过期时间戳一起存,每次进MainActivity时先校验过期时间,只剩下两三天时自动跳到登录页。这样做用户无感,代码也简单。
Retrofit拦截器统一加Token请求头:
public class AuthInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request original = chain.request(); String token = TokenManager.getInstance().getToken(); Request request = original.newBuilder() .header("Authorization", "Bearer " + token) .method(original.method(), original.body()) .build(); return chain.proceed(request); } }还有一点:HttpURLConnection和OkHttp不要混用。统一用OkHttp + Retrofit,否则你会在证书校验、超时配置上重复踩坑。Retrofit的GsonConverterFactory会自动把JSON解析成Java对象,但要注意日期格式的配置,FastJson和Gson对“2025-06-18 10:00:00”这种格式的解析策略不一样,后端返回的日期格式最好和前端约定死,比如统一用“yyyy-MM-dd HH:mm:ss”。
4.2 任务列表与多状态展示:RecyclerView的分页与空态
列表页是整个APP的门面。用RecyclerView展示任务列表,需要处理状态对应的样式、优先级排序、分页加载三个问题。
任务列表的排序规则建议:未完成的任务按优先级排序(高优先级在前),同优先级按截止时间排序;已完成的任务在列表底部按完成时间倒序。这个排序逻辑在后端SQL里做比在Android端做要省事得多,SQL里用ORDER BY字段加CASE WHEN表达式就能搞定:
SELECT * FROM t_task WHERE user_id = #{userId} ORDER BY CASE WHEN status = 2 THEN 1 ELSE 0 END, priority DESC, deadline ASC分页的交互很简单:下拉刷新加载第一页,上滑到底自动加载下一页。但要注意一个经典问题——快速滚动时如果用户触发了两次相同的加载请求,会产生重复数据。解决办法是加一个isLoading标志位,上一次请求没结束前不发起新请求。
实际上我更建议你直接接入Google的原生做法:Paging 3库。虽然学习成本略高,但在答辩环节你能讲出“我是用Paging库来处理分页数据和生命周期感知的”,技术分量马上就不同了。
任务列表的每个item,用CardView包裹,根据状态控制颜色:
if (task.getStatus() == 2) { titleView.setTextColor(Color.GRAY); titleView.setPaintFlags(titleView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); } else { titleView.setTextColor(Color.BLACK); }这个“已完成任务画删除线”的效果,虽然只是一个小交互,但演示时很加分。
4.3 新增与编辑任务:表单校验和日期选择不能省
新增任务的Activity里,最容易被忽略的是表单校验。很多人只做非空判断,但你要想清楚几个问题:
- 截止时间能不能早于当前时间?
- 提醒时间能不能晚于截止时间?
- 循环任务下,截止时间应该怎么算?
我在代码里加了如下校验逻辑:
private boolean validateTask(Task task) { if (TextUtils.isEmpty(task.getTitle())) { Toast.makeText(this, "任务标题不能为空", Toast.LENGTH_SHORT).show(); return false; } if (task.getDeadline() != null && task.getDeadline().before(new Date())) { Toast.makeText(this, "截止时间不能早于当前时间", Toast.LENGTH_SHORT).show(); return false; } if (task.getRemindTime() != null && task.getDeadline() != null && task.getRemindTime().after(task.getDeadline())) { Toast.makeText(this, "提醒时间不能晚于截止时间", Toast.LENGTH_SHORT).show(); return false; } return true; }日期选择器建议用MaterialDatePicker,比自带的DatePickerDialog好看,也要写更少的适配代码。注意MaterialDatePicker的返回时间戳是UTC零点,要转换成你本地时间再传后端,不然你会发现日期总是差一天。
4.4 本地提醒与循环任务:AlarmManager的启动和重启
提醒功能是任务管理APP的“灵魂”。很多同学做到最后一步才发现,借助AlarmManager定时触发通知并没有那么可靠——手机关机重启、APP被杀都会导致定时器失效。
我在实际测试中确认了一个让人头皮发麻的现象:很多国产手机系统(MIUI、EMUI)会默认限制第三方应用的自启动权限,锁屏一段时间后AlarmManager的闹钟会被系统休眠机制裁掉。此时你的应用必须引导用户去系统设置里手动开启“自启动”权限,代码写起来不难,但很碎:
Intent intent = new Intent(); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.parse("package:" + getPackageName())); startActivity(intent);核心的闹钟注册方式:
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); Intent intent = new Intent(this, TaskRemindReceiver.class); intent.putExtra("task_id", task.getId()); PendingIntent pendingIntent = PendingIntent.getBroadcast(this, task.getId().intValue(), intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); } else { alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent); }不要用setRepeating,它不精确,且在某些版本上会被系统自动对齐到省电窗口。循环任务的最佳做法是:每个循环任务只在下一次触发时间点设置一个闹钟,触发后由BroadcastReceiver做完任务处理后,再计算下一次触发时间并注册下一个闹钟,也就是用链式闹钟模拟循环。
5. 测试、打包与答辩准备的实战经验
5.1 模拟器测试的坑:为什么“在我电脑上跑得好好的”
进入联调阶段后,我第一周几乎每天都会遇到“模拟器上正常,真机上一堆问题”。后来总结出一个规律:模拟器掩盖了太多问题,一定要尽早用真机。
常用的坑有三个:
存储空间和适配问题。模拟器默认分辨率偏高,很多同学的UI在模拟器上看着完美,到真机小屏上就互相遮挡。建议适配至少三档屏幕:1080p标准屏、长屏(全面屏20:9)、平板或大屏设备。
网络问题。模拟器访问宿主机后端用10.0.2.2,真机访问电脑后端得用局域网IP。很多同学忘了改BASE_URL,导致真机一直连不上后端。我把BaseUrl放在BuildConfig里统一管理:
public class ApiClient { private static final String BASE_URL = BuildConfig.API_BASE_URL; // ... }然后在build.gradle里分别配置:
buildTypes { debug { buildConfigField "String", "API_BASE_URL", "\"http://192.168.1.100:8080/\"" } release { buildConfigField "String", "API_BASE_URL", "\"http://your-server.com/\"" } }SSL明文请求问题。调试时后端是HTTP协议,Android 9以上默认禁止明文流量。用networkSecurityConfig放行debug模式的明文访问,release再用HTTPS。
5.2 打包APK那些事:签名文件、ABI和v1/v2签名
打包APK看似简单,但很多人在签署APK这一步会因为粗心遇到问题。核心注意事项:
- 必须使用自己的签名文件(.jks),不要用Android Studio默认的debug.keystore上架或交给老师。创建一个正式的签名文件,
keyAlias和密码要牢记住,丢失就废了。 - 签名版本要勾选v1 + v2。如果只勾选v2,在Android 7.0以下的设备上安装会失败;只勾选v1,高版本系统会提示签名不安全。
- release构建时要用R8/ProGuard混淆,混淆规则文件里要加入Retrofit、Gson、OkHttp相关的keep规则,否则运行时报错或者JSON解析不了。
-keepattributes Signature -keepattributes *Annotation* -keep class com.example.task.entity.** { *; } -keepclassmembers class * { @com.google.gson.annotations.SerializedName <fields>; } -dontwarn okhttp.** -dontwarn retrofit2.** -keep class retrofit2.** { *; } -keep class sun.misc.Unsafe { *; }打包成功后,一定要在自己手机上装一次完整测试。我出现过release版本的按钮点击无反应,debug却正常的情况,后来发现是混淆把某个自定义Adapter的构造函数误删了。
5.3 论文与答辩:怎么把系统讲出层次感
答辩十分钟,重点不要花在三分钟的登录界面演示上,而是分三个层次讲:
第一层,讲需求分析。不要只说“用户需要一个待办清单”,要说明任务的状态、优先级、循环、提醒这四个核心概念来自对真实使用场景的分析。这一层老师关注的是你的思考过程。
第二层,讲架构设计。贴出系统架构图:Android端通过REST API与Spring Boot通信,Spring Boot通过MyBatis操作MySQL。传统分层架构展示完,立刻说清楚“为什么任务状态流转要放在后端而不是前端”,因为状态流转是业务规则,业务规则需要集中控制,防止多个客户端直接操作数据产生冲突。
第三层,讲难点与方案。把你做的循环任务、链式闹钟、状态机校验这几个亮点拿出来讲。这套组合拳打下来,老师问的问题基本不会超出这个范围了。
还需要准备一些容易被追问的问题:
- “完成任务后,提醒还会不会触发?”答案是完成状态切换时,要同时取消这个任务在AlarmManager里所有未触发的闹钟,并删除对应的PengingIntent。
- “多设备登录同一账号,任务怎么同步?”答案是你的系统以服务端数据为准,每次打开APP都刷新数据,不做本地离线的复杂同步,这是权衡后的取舍。
- “用户量大之后怎么办?”说明分页查询已经具备基本数据量应对能力,后续可以复用连接池、加Redis缓存,但这不是当前方案的核心重点。
5.4 一份看起来更“高级”的个人体会
做完这个项目,我对工程化最深刻的感受是:优先保证数据的确定性,再去优化体验。任务状态改错了、提醒时间算错了,这些是数据确定性错误,比界面丑、加载慢要致命得多。把最能保证确定性的逻辑放在后端而不是UI层,可以避免很多奇怪的并发问题。第二个体会是,项目里一些看起来平平无奇的字段设计,比如把deadline和remind_time分开,一开始可能觉得多余,但真正做到提醒功能时才发现,这不只是数据规范性问题,而是业务逻辑的基本盘,任务管理系统的复杂度和成熟度,很大程度上就看能不能把这些“小事”想清楚。
最后,如果你也在做这个题目,最值得花时间的两个功能模块是状态流转和循环提醒。把这两个模块讲透,你的系统性思考能力、业务建模能力和动手能力都能在答辩时清晰地展现出来。祝顺利。