Android+SpringBoot日记APP毕设实战:从数据库到接口的完整实现
2026/9/11 8:06:21 网站建设 项目流程

1. 为什么我选“日记APP”当毕设:需求拆解与选型逻辑

每年到了毕设开题的时候,总有人问我:“Java方向的毕设做什么最简单又能稳过?”我的回答一直是:做工具类APP,尤其个人日记本这种,性价比极高。

先说原因。个人日记本这类项目,功能边界非常清晰,无非就是用户的注册登录、日记的增删改查、心情记录、图片附件、时间线展示这几件事。它的业务复杂度适中,能充分展示你的工程能力,又不至于把自己绕进高并发的泥潭里。更重要的是,它天然就是一个前后端分离的完整系统——Android作为移动端展示与交互,SpringBoot作为后端服务提供接口,两边各司其职,工作量均衡,最能体现“我有全栈思维”这个点。答辩的时候,老师问“你这个系统架构是什么”,你可以坦然回答:“Android客户端负责UI和本地数据缓存,SpringBoot负责业务逻辑和持久化,两端通过RESTful API通信”,这个回答本身就是完整的项目概括。

再说选型。用户给的标题里已经锁死了技术栈:客户端是Android原生,服务端是SpringBoot,数据库层面配合MyBatis。我在实际开发里验证过这套组合,确实适合毕设场景——Android原生生态成熟,资料多;SpringBoot自动配置极大减少了搭建成本;MyBatis的SQL可控性强,遇到复杂查询可以手写,不像JPA那样遇到坑不好排查。所以我的建议是:别折腾微服务,别上Redis缓存,就把这套基础组合做到极致,功能完整度比技术炫技在答辩中更值钱。

整套系统拆开来看,核心功能就这些:

  • 用户模块:注册、登录、个人信息维护,登录态用Token保持
  • 日记模块:新增、编辑、删除、分页查询、按日期/关键词搜索
  • 情绪标注:每篇日记可标记心情,比如开心、平淡、低落、愤怒
  • 图片附件:拍照或相册选图,随日记一起上传
  • 统计展示:查看一周/一月的心情变化曲线,这样“智能”二字就落到了实处
  • 本地缓存:无网络环境下也能看近期日记,网络恢复后自动同步

这套功能设计下来,既覆盖了常规CRUD,又有“心情统计”“本地缓存”这种可以讲出亮点的模块,无论你的论文写“系统设计”还是“实现难点”,都有素材。

2. 数据库与接口先行:把日记系统的地基打牢

我做过不少毕设辅导,发现很多同学的习惯是“先写代码再做表”,结果到联调阶段被字段对不上、接口设计不合理折磨到崩溃。正确的做法应该是:先把ER图和数据表结构定下来,再把接口契约列清楚,最后才动手写代码。

2.1 数据表设计与ER图关键点

我的项目里一共设计了三张核心表,简洁但覆盖所有业务需求。

用户表 t_user:

字段名类型说明
idBIGINT主键,自增
usernameVARCHAR(50)用户名,唯一索引
passwordVARCHAR(100)加密后的密码
nicknameVARCHAR(50)昵称
avatarVARCHAR(255)头像URL
create_timeDATETIME注册时间

密码加密是必须做的,哪怕只是毕设答辩,也不能明文存密码。我用的是Spring Security的BCryptPasswordEncoder,这个加密方式自带盐值,相同密码每次加密结果都不一样,安全性足够。

日记表 t_diary:

字段名类型说明
idBIGINT主键
user_idBIGINT关联用户ID,普通索引
titleVARCHAR(100)日记标题
contentTEXT正文内容
moodTINYINT心情状态,0-4对应五档
weatherVARCHAR(20)天气,如晴、多云、雨
image_urlsVARCHAR(1000)图片URL,多个用逗号拼接
is_deleteTINYINT逻辑删除标记,0正常1删除
create_timeDATETIME创建时间
update_timeDATETIME最后修改时间

看到is_delete这个字段了吗?这是我在实际项目中踩坑换来的经验。物理删除会让“回收站”“数据恢复”这些功能后续没法扩展,而且误删数据找不回来。毕设项目用逻辑删除是加分项,答辩时提到“我做了逻辑删除而非物理删除,保证数据可回溯”,老师会认为你有工程意识。

心情统计表(或视图):我这里的统计没有额外建表,而是通过SQL对t_diary表按mood和create_time进行GROUP BY聚合查询。这样做数据最实时,也省去维护统计表的麻烦。ER图中体现出user与diary的一对多关系即可,写论文时这是必画的图。

2.2 SpringBoot接口设计:RESTful风格的统一约定

接口设计遵循RESTful风格,统一返回Result对象。我在项目里定义了一个通用返回体:

public class Result<T> { private Integer code; // 200成功,500失败,401未授权 private String message; // 提示信息 private T data; // 业务数据 }

所有接口一律返回这个结构,Android端解析JSON时统一处理code,这样错误处理逻辑就收敛到了一处,不会出现“接口返回格式不统一、客户端解析乱七八糟”的问题。

核心接口列表如下:

方法路径说明
POST/api/user/register用户注册
POST/api/user/login用户登录,返回Token
GET/api/diary/page分页查询日记列表
GET/api/diary/{id}获取日记详情
POST/api/diary新增日记
PUT/api/diary/{id}修改日记
DELETE/api/diary/{id}逻辑删除
GET/api/diary/search关键词搜索
GET/api/diary/mood/stats心情统计
POST/api/upload图片上传

关于登录态,我用的是JWT(JSON Web Token),而不是传统的Session。Android端登录成功后拿到Token,存到SharedPreferences里,后续每个请求放在Header的Authorization字段中。后端通过拦截器校验Token,解析出对应的userId,再处理业务请求。这么做的好处是服务端无状态,以后想横向扩展也不受Session黏滞限制。

2.3 一个关键细节:字段校验与异常处理

接口看似简单,但真正的坑在细节里。比如用户注册时,用户名是否重复?密码是否为空?日记的content能不能为空?如果这些不做校验,前端传个空字符串过来,数据库存了半天,查询端还要到处判空,后患无穷。

SpringBoot处理这个问题的标准姿势是用@Validated注解配合@NotBlank@Size这些Java Bean Validation规范:

@PostMapping("/register") public Result<String> register(@RequestBody @Validated UserRegisterDTO dto) { // dto.username 非空,dto.password 长度6-20位 // 校验不通过时,框架自动抛出MethodArgumentNotValidException }

我在全局异常处理器里捕获所有校验异常,统一封装成Result返回给客户端。客户端拿到统一的code,弹Toast提示,体验就非常干净。这套组合写进论文也算是一个“系统鲁棒性设计”的点位。

3. Android客户端核心模块实现:从登录到日记列表的完整链路

Android端的开发,我用的语言是Java(标题里明确说了Java),IDE是Android Studio。在动手之前想清楚一个问题:客户端的数据源是只走后端接口,还是本地SQLite与后端并存?

我的方案是:核心数据走后端接口,但本地用SQLite做缓存,网络不可用时降级读取本地缓存。这个方案比纯网络请求复杂一些,但“离线可用”这四个字在功能性上很有说服力。下面按开发顺序拆解各模块。

3.1 网络层封装:Retrofit2 + OkHttp + 统一拦截器

网络层是整个APP的骨架。我用Retrofit2作为HTTP客户端框架,配合OkHttp拦截器实现Token注入和日志打印。

首先定义一个拦截器,每次请求自动带上Token:

public class AuthInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request original = chain.request(); String token = TokenManager.getInstance().getToken(); if (token != null) { Request request = original.newBuilder() .header("Authorization", token) .method(original.method(), original.body()) .build(); return chain.proceed(request); } return chain.proceed(original); } }

TokenManager就是一个封装了SharedPreferences读写的小工具,单例模式,全局可访问。填Token这步做好之后,业务层的每个API接口方法就不用重复传Token参数了。

Retrofit接口定义示例:

public interface ApiService { @POST("api/user/login") Call<Result<LoginResponse>> login(@Body LoginRequest request); @GET("api/diary/page") Call<Result<PageResult<Diary>>> getDiaryPage(@Query("page") int page, @Query("size") int size); @POST("api/diary") Call<Result<Long>> addDiary(@Body Diary diary); @Multipart @POST("api/upload") Call<Result<String>> uploadImage(@Part MultipartBody.Part file); }

这里要强调一个容易踩的坑:Retrofit的BaseURL必须以“/”结尾,否则运行时会报IllegalArgumentException。我第一次搭的时候就是漏了结尾斜杠,排查了半小时。

3.2 登录页与Token持久化

登录页的布局很简单:用户名、密码、登录按钮、跳转注册页的链接。但逻辑不能只做“请求成功就跳转”——错误码的提示必须到位。比如用户密码错了,后端返回code=500且message=“用户名或密码错误”,客户端必须把这个message展示出来,而不是笼统提示“请求失败”。

登录成功后:

private void handleLoginSuccess(LoginResponse response) { TokenManager.getInstance().saveToken(response.getToken()); TokenManager.getInstance().saveUserId(response.getUserId()); Intent intent = new Intent(LoginActivity.this, MainActivity.class); startActivity(intent); finish(); }

Token持久化是登录态的核心,我在SharedPreferences之外,还给SharedPreferences本身加了一层加盐BASE64编码,防止Root设备上明文被读出来。说实话毕设项目中很少人会注意到这个细节,但论文里写“对敏感信息进行二次编码存储”,评审老师的印象分会不一样。

3.3 日记列表页:RecyclerView + 多类型布局 + 下拉刷新

日记列表是APP的主界面,我用的是SwipeRefreshLayout包裹RecyclerView的方案。Adapter继承RecyclerView.Adapter,在onBindViewHolder里把标题、正文截取片段、心情图标、日期填充进去。

日记列表一个值得注意的点是:正文在列表中只显示截取的摘要,而不是全部文本。我在后端查询时直接通过SQL截取前80个字符返回摘要字段,一方面减少网络传输量,一方面客户端渲染更快。这是典型的“接口设计为UI服务”的思路。

下拉刷新逻辑也很关键。SwipeRefreshLayout的OnRefreshListener里重新请求第一页数据,成功时调用adapter.setDataswipeRefreshLayout.setRefreshing(false)。分页加载则通过RecyclerView的滑动监听,在滑到底部前一个Item时触发加载下一页。

这个页面的状态处理我踩过很多次:请求失败、加载完毕无更多数据、首次加载 loading 中,这三种状态必须有对应的UI表现。否则用户网络不好时点进来,白屏加转圈,体验极差。我的做法是一个FrameLayout里放三四个View,根据状态切换visibility。虽然实现笨一点,但稳。

3.4 写日记页:富文本编辑与图片选择

写日记页面是另一个技术点较多的模块。我的设计是:顶部标题EditText,中间正文EditText(支持多行),底部一排图标按钮(心情选择、天气选择、图片选择)。

图片选择我用的是系统Intent调起相册或相机,然后用Glide加载到ImageView显示。选完图片要上传时,有个大坑:跨Android版本的文件路径处理。Android 10起,直接拿相册返回的data.getData()去构造File会碰到权限问题,必须通过ContentResolver解析URI拿到真实路径或直接读输入流。

我的上传代码在Android 10以上是用getContentResolver().openInputStream(uri)读取输入流,包装成MultipartBody.Part:

InputStream is = getContentResolver().openInputStream(uri); byte[] bytes = readAllBytes(is); RequestBody body = RequestBody.create(MediaType.parse("image/*"), bytes); MultipartBody.Part part = MultipartBody.Part.createFormData("file", "diary_" + System.currentTimeMillis() + ".jpg", body);

这样做不依赖外部存储权限,适配性好。Android 6以上的运行时权限申请也需要提前处理,我封装了一个PermissionHelper类,统一处理相机、相册、存储权限的申请回调。

3.5 本地SQLite缓存:Room还是原生SQLite?

本地缓存方案我在Room和原生SQLite之间纠结过。Room需要引入Room Runtime和Room Compiler两个依赖,并且需要定义Entity、Dao、Database三层,代码量不小;原生SQLiteOpenHelper简单直接,但代码写起来相对繁琐,查询要自己拼SQL。

我最终选的方案是:网络层返回的日记列表直接以JSON形式缓存到本地文件,用SharedPreferences记录缓存时间,而不是建SQLite表存结构化的日记数据。原因很务实:日记列表的缓存scene比较单一,读出来反序列化成List 直接能用,不用建表、不用SQL映射,代码量少一半。而且日记本身属于低频更新数据,JSON文件的整体替换策略已经够用。

如果你的论文想突出“数据持久化”,那就用Room把Diary实体映射到本地表,再写一个简单的同步逻辑。两种路线都可以,但不要两边都做不彻底。

4. 让日记不只是文本:心情标注、图片附件与统计曲线的功能增强

CRUD人人会写,真正让一个毕设项目从“及格”到“良好”甚至“优秀”的,是那些能体现设计思考的特色功能。这一节我拆解三个增强模块的实现思路。

4.1 心情标注与天气记录

日记不只是文字,它承载的是用户彼时的情绪和场景。所以我在日记里加了两个轻量字段:mood(心情枚举)和weather(天气字符串)。

心情用TINYINT存储,对应5个等级:

含义图标
0低落灰色乌云
1平淡蓝色平静
2还好黄色一般
3开心橙色笑脸
4兴奋红色太阳

写日记的页面中,心情选择是一排单选图标,选中状态高亮,存进Diary对象。列表页中根据mood值加载不同的表情图标。这个功能虽然简单,但“把情绪结构化”这一点在论文的需求分析里很出彩。

4.2 图片上传与显示

图片上传的接口用了Multipart格式,SpringBoot端的接收逻辑:

@PostMapping("/api/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error("文件为空"); } String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String filename = UUID.randomUUID().toString().replace("-", "") + ext; // 保存到服务器指定的上传目录 File dir = new File(System.getProperty("user.dir") + "/upload/"); if (!dir.exists()) dir.mkdirs(); file.transferTo(new File(dir, filename)); return Result.success("/upload/" + filename); }

上传文件保存到服务器本地目录,返回可访问的URL,Android端用Glide加载。毕设场景不需要考虑分布式存储,本地磁盘足够。但要注意:SpringBoot默认上传文件大小限制是1MB,必须通过配置调大,否则选个大图直接报错。

spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=50MB

还有一个细节是图片显示前必须用Glide做加载占位和失败占位,否则弱网环境下图片区域一片空白,体验很突兀。

4.3 心情统计曲线:把SQL聚合结果变成可视化图表

心情统计是本项目里“智能”二字的最佳载体。后端的统计接口通过以下SQL,按日期和心情值聚合出近7天的分布:

SELECT DATE(create_time) AS day, mood, COUNT(*) AS cnt FROM t_diary WHERE user_id = #{userId} AND is_delete = 0 AND create_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY DATE(create_time), mood ORDER BY day

我在这里用了一个小技巧:聚合查询在内存中整理成7x5的二维数组结构,天数做行、心情等级做列,没数据的格子补0。这是为了让Android端绘图时不用处理稀疏数据,直接拿到完整的矩阵就能画堆叠柱状图。

Android端图表绘制我用了MPAndroidChart(GitHub上的开源图表库),添加依赖后,设置X轴标签为日期,Y轴为数量,一个多小时就能完成:

implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'

画图的关键坑是X轴Label过多时会显示重叠,需要设置setLabelCount(7)并强制setGranularity(1f)。另外一个很丑的技术问题是:如果某天没有任何日记,柱状图中间会空一块,视觉上非常难看,所以后端补齐0的策略在这里发挥了作用,图形看起来连续、专业。

5. 联调、打包与答辩:真实工程里的坑与经验

前四节把整个系统的核心模块梳理了一遍。这一节聊点“文档里不写”的东西——联调阶段容易踩的坑、APP打包的注意事项,以及答辩时老师常问的问题。这些经验是我自己走过弯路之后总结出来的,能帮你少折腾好几天。

5.1 联调阶段最常见的三个问题

第一个:Android模拟器的网络地址问题。

如果你用Android Studio自带模拟器,访问电脑本机的SpringBoot服务,不能用localhost127.0.0.1,因为模拟器里的这个地址指向的是模拟器自身。正确写法是http://10.0.2.2:8080,这是模拟器映射到宿主机的一个特殊地址。我当时忘了这一点,请求一直超时,还以为是后端端口被占用,排查了半下午。真机调试的话,直接把地址改成电脑的局域网IP,但要保证手机和电脑连同一个WiFi。

第二个:数据库时区问题。

SpringBoot连接MySQL,如果JDBC URL没有加serverTimezone=Asia/Shanghai,夜里0点附近插入日记,存储的时间可能比本地时间少了8小时。因为MySQL驱动默认用的是UTC。解决方案:

spring.datasource.url=jdbc:mysql://localhost:3306/diary?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

时区问题在时间线排序时尤其明显——你以为用户晚上11点写的生活记录,排序却跑到了第二天凌晨,答辩演示时出现这个bug会非常尴尬。

第三个:Android的明文HTTP流量限制。

Android 9(API 28)开始,默认禁止明文HTTP请求。如果你的后端是http://不是https://,请求会直接被拦截并抛出流明错误。解决方式是在AndroidManifest.xml的application节点里加上:

android:usesCleartextTraffic="true"

或者针对调试环境配置networkSecurityConfig只允许特定域名的HTTP请求。毕设场景直接加usesCleartextTraffic最省事,但写论文时如果需要体现安全意识,可以提一句“生产环境建议配置HTTPS”。

5.2 APP签名打包与安装

开发阶段跑Debug包没问题,但答辩演示最好打一个Release安装包,免得现场连接AS的时候环境出幺蛾子。

生成签名APK的步骤是:Build -> Generate Signed Bundle / APK -> 选择Create New Key Store,填好别名、密码、国家代码后Next,等到Gradle构建完成,产物就在app/release/目录下。

一个值得注意的坑是:Release包的ProGuard / R8混淆配置不当会导致Gson解析实体类全变成null。因为实体类的字段被混淆成了a、b,而Gson用的是反射按字段名解析JSON。解决方案是给实体类所在的包单独关闭混淆,或者在proguard-rules.pro里加规则:

-keep class com.example.diary.entity.** { *; } -keep class com.example.diary.dto.** { *; }

另外要记得在build.gradle里把混淆开关配上:

buildTypes { release { minifyEnabled false // 项目不大,直接关掉混淆,图个稳定 proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } }

我实际打Release包时把minifyEnabled暂时设成false,等到论文写“系统安全设计”时再提混淆方案——毕竟演示阶段稳定性第一。

5.3 答辩高频问题与应对思路

根据我当过多届毕设评审“亲友团”的观察,老师对这类项目的提问方向是固定的,提前准备这几个问题的答案,答辩基本稳:

“为什么用JWT而不用Session?”

答:JWT无状态,服务器不保存会话信息,天然适合前后端分离和横向扩展。用户登录后得到Token,客户端每次请求携带,服务端只做验签。不需要Redis保存Session,也不需要处理Session过期与迁移问题。

“逻辑删除和物理删除的区别?”

答:物理删除是真正从数据库里DELETE掉记录,无法恢复;逻辑删除是UPDATE一个标记位,查询时过滤掉已删除数据。我采用逻辑删除是为了保留用户所有历史数据,防止误删,也方便后续扩展回收站功能。代价是每张业务表多一个字段,查询时多一个过滤条件。

“心情统计的数据一致性怎么保证?”

答:统计不单独建表,直接基于diary表的mood和create_time字段通过SQL实时聚合。这样保证统计结果永远不会与日记数据不一致——因为数据源是同一个。缺点是聚合查询在大数据量下性能会下降,但个人日记场景的量级完全不会触发这个问题。

“离线缓存为什么用JSON文件,不建SQLite?”

答:日记列表读取场景是整体读写,JSON文件整体序列化和反序列化最简单,不用建表维护字段映射。如果未来要做复杂条件查询,就可以切换到Room,在数据访问层做抽象替换,业务层不受影响。

5.4 初始数据长的不好看怎么办

这块属于血泪建议,一定要在答辩前给账号里先写好8-10篇内容充实的日记,最好是从当天往前连续覆盖两周的数据,每天的mood尽量不同。因为老师点开APP第一眼看的是列表和统计页,如果数据稀疏,统计曲线全是0,整体demo效果直接打折。你可以写点技术学习感悟类的随笔,比如“今天学会了Retrofit拦截器”“SQL里的DATE_SUB函数真好用”,内容量和日期分布都要有,这样演示时下拉刷新、统计曲线动起来特别有说服力。

数据库初始化脚本里也可以造一批测试数据,在SQL文件里写好INSERT语句,论文附录还能附上,答辩时直接展示“我造了一批多维度测试数据验证接口”。

5.5 如何把项目包装成“亮点型毕设”

最后分享一个思路层面的心得:同样的功能,表达方式不同,分值完全不同。“实现了一个日记APP”和“实现了支持离线缓存、心情可视化的移动端个人知识管理工具”,描述的是几乎同样的代码,但后者明显更有吸引力。我写论文时把每个技术决策都包装成了“为什么这样选”的问题来组织,比如:

  • 为什么用JWT而不是Session —— 无状态设计,支撑多端登录
  • 为什么图片用UUID重命名 —— 防止文件名冲突与路径遍历攻击
  • 为什么做逻辑删除 —— 数据可回溯,用户体验有保障
  • 为什么统计用实时聚合 —— 保证数据一致性,架构简单

这套“设计决策 -> 技术方案 -> 收益说明”的叙事结构,不仅写论文好用,答辩的时候被问“你为什么这么设计”时,你也能从容回答,而不是只能说“大家都这么搞”。

做毕设最怕的不是功能多复杂,而是功能做完了说不清楚。把每个模块背后的“为什么”想透,你的项目就已经超过一半的人了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询