ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Android+SpringBoot日记APP毕设实战:从数据库到接口的完整实现

2026/9/11 8:06:32 拓冰建站 浏览量
Android+SpringBoot日记APP毕设实战:从数据库到接口的完整实现 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)头像URLcreate_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 ResultT { private Integer code; // 200成功500失败401未授权 private String message; // 提示信息 private T data; // 业务数据 }所有接口一律返回这个结构Android端解析JSON时统一处理code这样错误处理逻辑就收敛到了一处不会出现“接口返回格式不统一、客户端解析乱七八糟”的问题。核心接口列表如下方法路径说明POST/api/user/register用户注册POST/api/user/login用户登录返回TokenGET/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图片上传关于登录态我用的是JWTJSON Web Token而不是传统的Session。Android端登录成功后拿到Token存到SharedPreferences里后续每个请求放在Header的Authorization字段中。后端通过拦截器校验Token解析出对应的userId再处理业务请求。这么做的好处是服务端无状态以后想横向扩展也不受Session黏滞限制。2.3 一个关键细节字段校验与异常处理接口看似简单但真正的坑在细节里。比如用户注册时用户名是否重复密码是否为空日记的content能不能为空如果这些不做校验前端传个空字符串过来数据库存了半天查询端还要到处判空后患无穷。SpringBoot处理这个问题的标准姿势是用Validated注解配合NotBlank、Size这些Java Bean Validation规范PostMapping(/register) public ResultString register(RequestBody Validated UserRegisterDTO dto) { // dto.username 非空dto.password 长度6-20位 // 校验不通过时框架自动抛出MethodArgumentNotValidException }我在全局异常处理器里捕获所有校验异常统一封装成Result返回给客户端。客户端拿到统一的code弹Toast提示体验就非常干净。这套组合写进论文也算是一个“系统鲁棒性设计”的点位。3. Android客户端核心模块实现从登录到日记列表的完整链路Android端的开发我用的语言是Java标题里明确说了JavaIDE是Android Studio。在动手之前想清楚一个问题客户端的数据源是只走后端接口还是本地SQLite与后端并存我的方案是核心数据走后端接口但本地用SQLite做缓存网络不可用时降级读取本地缓存。这个方案比纯网络请求复杂一些但“离线可用”这四个字在功能性上很有说服力。下面按开发顺序拆解各模块。3.1 网络层封装Retrofit2 OkHttp 统一拦截器网络层是整个APP的骨架。我用Retrofit2作为HTTP客户端框架配合OkHttp拦截器实现Token注入和日志打印。首先定义一个拦截器每次请求自动带上Tokenpublic 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) CallResultLoginResponse login(Body LoginRequest request); GET(api/diary/page) CallResultPageResultDiary getDiaryPage(Query(page) int page, Query(size) int size); POST(api/diary) CallResultLong addDiary(Body Diary diary); Multipart POST(api/upload) CallResultString uploadImage(Part MultipartBody.Part file); }这里要强调一个容易踩的坑Retrofit的BaseURL必须以“/”结尾否则运行时会报IllegalArgumentException。我第一次搭的时候就是漏了结尾斜杠排查了半小时。3.2 登录页与Token持久化登录页的布局很简单用户名、密码、登录按钮、跳转注册页的链接。但逻辑不能只做“请求成功就跳转”——错误码的提示必须到位。比如用户密码错了后端返回code500且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.setData和swipeRefreshLayout.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.PartInputStream 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 ResultString 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); }上传文件保存到服务器本地目录返回可访问的URLAndroid端用Glide加载。毕设场景不需要考虑分布式存储本地磁盘足够。但要注意SpringBoot默认上传文件大小限制是1MB必须通过配置调大否则选个大图直接报错。spring.servlet.multipart.max-file-size10MB spring.servlet.multipart.max-request-size50MB还有一个细节是图片显示前必须用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端图表绘制我用了MPAndroidChartGitHub上的开源图表库添加依赖后设置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服务不能用localhost或127.0.0.1因为模拟器里的这个地址指向的是模拟器自身。正确写法是http://10.0.2.2:8080这是模拟器映射到宿主机的一个特殊地址。我当时忘了这一点请求一直超时还以为是后端端口被占用排查了半下午。真机调试的话直接把地址改成电脑的局域网IP但要保证手机和电脑连同一个WiFi。第二个数据库时区问题。SpringBoot连接MySQL如果JDBC URL没有加serverTimezoneAsia/Shanghai夜里0点附近插入日记存储的时间可能比本地时间少了8小时。因为MySQL驱动默认用的是UTC。解决方案spring.datasource.urljdbc:mysql://localhost:3306/diary?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai时区问题在时间线排序时尤其明显——你以为用户晚上11点写的生活记录排序却跑到了第二天凌晨答辩演示时出现这个bug会非常尴尬。第三个Android的明文HTTP流量限制。Android 9API 28开始默认禁止明文HTTP请求。如果你的后端是http://不是https://请求会直接被拦截并抛出流明错误。解决方式是在AndroidManifest.xml的application节点里加上android:usesCleartextTraffictrue或者针对调试环境配置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重命名 —— 防止文件名冲突与路径遍历攻击为什么做逻辑删除 —— 数据可回溯用户体验有保障为什么统计用实时聚合 —— 保证数据一致性架构简单这套“设计决策 - 技术方案 - 收益说明”的叙事结构不仅写论文好用答辩的时候被问“你为什么这么设计”时你也能从容回答而不是只能说“大家都这么搞”。做毕设最怕的不是功能多复杂而是功能做完了说不清楚。把每个模块背后的“为什么”想透你的项目就已经超过一半的人了。