又到一年毕设季,每年这个时候“Java Spring Boot + Android旅游攻略系统”这类题目都会被反复翻出来。这个题目热度高不是没道理的——它覆盖了后端、移动端、数据库三大块,难度适中,技术栈通用,既能锻炼完整项目能力,又不至于让大部分同学卡死在底层细节里。我的这个项目不是那种随手写的demo,而是把用户端+后台管理端、旅游景点、攻略文章、评论、收藏、分类搜索这些功能都做了闭环,配合完整的数据库设计和接口文档,适合拿来当课程设计、毕业设计,甚至后续找工作时的项目经验。
能做什么,先给你一个直观感受:Android端打开App,可以注册登录、浏览景点列表、查看景点详情、阅读旅游攻略、发表评论、收藏喜欢的内容、按关键词搜索;后台管理端(网页)可以维护景点信息、审核攻略文章、管理用户和评论数据。前后端数据通过RESTful API交互,整个项目跑起来之后,一套完整业务流程是通的。适合谁用呢,基础是学了Java和数据库原理、但对Spring Boot和Android开发还比较陌生的同学,或者自己有项目经验、想快速套一个完整系统来应付毕设/面试的人。
这套系统我最早是给自己带的几个学生做的,后来陆陆续续改了好几版,踩了不少坑。今天就把整个设计思路、核心实现、实操过程、以及那些文档里不会写的问题排查经验全部整理出来,你照着这个思路走,能省掉大量自己摸索的时间。
1. 项目整体设计与思路拆解
1.1 系统定位与技术选型
毕设项目的核心矛盾永远是“工作量够不够”和“能不能跑通”。很多同学上来就想上微服务、Redis缓存、消息队列,结果自己把自己坑了。旅游攻略系统本质是一个内容型应用,核心数据模型是“用户-景点-攻略文章-评论-收藏”,这个规模下单体应用就是最合理的架构。
我的技术选型这样定:
- 后端:Spring Boot 2.x + MyBatis-Plus + MySQL 5.7,构建 RESTful API
- 移动端:Android(原生Java开发),网络层用Retrofit + Gson,图片加载用Glide
- 管理端:为了控制工作量,直接复用后端接口,用简单的Thymeleaf页面做后台管理界面
为什么不用Vue写管理端?因为对大部分没接触过前端工程化的同学来说,Vue + ElementUI那套Node环境、跨域配置、打包部署的坑,比后端本身还多。Thymeleaf是Spring Boot原生支持的模板引擎,语法简单,一个Spring Boot项目就能同时搞定API和后台页面,省掉一整个前端的部署复杂度。
Spring Boot 2.x而不是3.x,这也是一个重要的考虑。Spring Boot 3要求JDK 17,很多学校机房和教程环境还停留在JDK 8,而且大量第三方组件对3.x的兼容并不完善。我实测下来2.7.x版本最稳定,组件兼容性也最好。记住一个原则:毕设项目不追新版本,追的是“跑得起来、答辩稳得住”。
1.2 功能模块规划
整个系统的功能模块可以拆成两大端,清晰分离开:
用户端(Android App):
- 注册登录:手机号/用户名 + 密码,登录成功后返回token保持会话状态
- 首页景点列表:分页加载,支持按分类筛选和关键词搜索
- 景点详情:图文介绍、评分、地址、开放时间、所属分类
- 攻略模块:查看攻略列表、攻略详情(富文本内容)、发表攻略
- 评论与收藏:对景点发表评论、点赞/取消点赞、收藏景点
- 个人中心:我的收藏、我的评论、我的攻略、修改资料
后台管理端(Web页面):
- 管理员登录
- 景点管理:增删改查、上下架、上传景点图片
- 攻略管理:审核/删除用户发布的攻略
- 评论管理:删除违规评论
- 用户管理:查看用户列表、禁用账号
这个功能划分有几个巧思:后端的CRUD四件套(增删改查)完整覆盖了;用户端和管理端都有了;图片上传、权限控制(用户/管理员两种角色)、关联查询(景点关联攻略、评论关联用户)都涉及了,这些点恰好是答辩时老师最喜欢问的,也刚好是评分表里的加分项。
1.3 数据库表设计思路
数据库设计往往是先画ER图再建表,但实际写代码时我习惯反过来——先思考接口需要什么数据,再决定表结构。因为接口的输入输出决定了表的字段,表驱动接口容易导致接口设计别扭。
核心表一共5张:
- user表:用户信息,区分role字段(0普通用户,1管理员)
- scenic表:景点信息,包含标题、描述、详情、图片、分类、评分、地址
- article表:攻略文章,包含标题、正文、封面、作者id(关联user)、审核状态
- comment表:评论,包含评论内容、用户id、景点id、评论时间
- favorite表:收藏关系表,用户id + 景点id联合唯一
这5张表基本能把所有业务串起来。我见过很多同学把表设计得特别散,一看就是故意凑数量,反而让关联查询复杂得不行。表设计讲究的是朴素正确,不是花哨。
2. 核心细节解析与实操要点
2.1 Spring Boot后端:从配置到接口的核心细节
后端工程结构上,我用标准的三层架构:Controller(接口层)- Service(业务层)- Mapper(数据访问层),这是大家都懂的结构,但有几个细节你要注意。
application.yml是后端的命门。我习惯把数据库配置、端口配置、MyBatis-Plus日志、文件上传大小全部集中管理:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/travel_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: auto这里有两个藏在细节里的重点。第一,serverTimezone=Asia/Shanghai是必须加的,不加就会遇到“数据库连接失败”的经典报错,而且报错提示和真正原因完全对不上,能卡住你半天。第二,map-underscore-to-camel-case这个配置非常关键,它让数据库的create_time字段能直接映射到Java实体类的createTime属性,不用每处都写@TableField注解。
MyBatis-Plus的选型也是一个很实际的决定。相比原生MyBatis需要手写大量XML映射文件,MyBatis-Plus内置了CRUD方法:selectById、selectPage、insert、updateById这些基础操作直接可用,省掉大约60%的重复代码。复杂查询再通过Wrapper条件构造器解决,比如搜索景点:
QueryWrapper<Scenic> wrapper = new QueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(keyword), "name", keyword) .eq(categoryId != null, "category_id", categoryId) .orderByDesc("create_time"); Page<Scenic> page = new Page<>(current, size); scenicMapper.selectPage(page, wrapper);这个方法不仅省了SQL,而且MyBatis-Plus的Wrapper自带条件判断(第一个参数为false时自动忽略该条件),不用自己写一堆if判断拼接SQL,代码清爽得多。
接口层需要统一返回格式。我定义了一个通用返回体Result,包含code(状态码)、msg(提示信息)、data(数据),所有接口统一返回这个格式。这样做的好处是Android端解析JSON时只用处理一种结构,而且可以统一拦截业务错误。
2.2 Android端开发的关键实现点
Android端是很多人真正头疼的地方,因为安卓开发涉及UI、网络、线程、本地存储一堆东西交织在一起。我的经验是把它拆成几层,逐层处理。
网络层用Retrofit,接口定义为Java接口,把后端每个URL映射成方法。举例,用户登录的接口:
public interface ApiService { @POST("user/login") Call<ResultVO<User>> login(@Body LoginRequest request); @GET("scenic/page") Call<ResultVO<PageResult<Scenic>>> getScenicPage( @Query("current") int current, @Query("size") int size ); }使用的时候要记住:网络请求必须放在子线程,Retrofit的enqueue方法内部已经做了异步处理,回调方法自动切回主线程,这个机制要理解透,不要去手动开线程,容易乱套。
登录状态保持,我用的是SharedPreferences保存token。用户登录成功后,后端返回token和一个user对象,Android端把token存起来,之后的每次请求都在拦截器里给请求头加上token:
Request.Builder builder = originalRequest.newBuilder() .header("token", token) .method(originalRequest.method(), originalRequest.body());这个拦截器是OkHttp提供的功能,位于Retrofit底层。实现这个拦截器之后,不用在每个接口调用处手动传token,全局自动生效,能省事不少。
UI层面,列表页用RecyclerView + FastAdapter或BaseQuickAdapter。图片加载统一用Glide,一行代码解决网络图片的加载、缓存、占位图问题,不需要自己管理线程池和缓存。地图这块如果有需要(比如景点位置展示),可以接高德地图SDK,但要注意注册开发者账号拿到Key,这个Key要在清单文件和代码里都配置。
Android还有一个绕不开的坑是异步更新UI。很多人第一次写的时候,会在子线程里直接改UI控件,结果应用闪退。Android原生开发规定必须在主线程更新UI,Retrofit的enqueue回调已经自动切回主线程了,但如果你用了RxJava或者自己开线程,就得手动runOnUiThread。
2.3 图片上传和富文本内容存储
景点图片和攻略封面是另一个容易忽视的部分。我的做法是:Android端把图片文件转成Base64编码字符串,通过JSON传给后端;后端收到后解码成图片文件,保存到服务器本地,返回图片访问URL给前端。这种方法稳定,不需要额外搭建OSS或文件服务器,毕设场景的数据量完全够用。
// 后端接收Base64图片 public String uploadImage(@RequestBody Map<String, String> body) { String base64Data = body.get("base64Image"); // 去掉data:image/jpeg;base64, 前缀 byte[] bytes = Base64.getDecoder().decode(base64Data.split(",")[1]); String fileName = UUID.randomUUID().toString() + ".jpg"; File file = new File(uploadDir, fileName); FileUtils.writeByteArrayToFile(file, bytes); return "/upload/" + fileName; }攻略文章的正文,我用的是HTML格式。Android端的WebView控件可以直接渲染HTML内容,不需要引入很重的富文本编辑器。用户发布攻略时,在App里也是用WebView配合一个简单的HTML编辑器实现,这样存到数据库里就是这个HTML字符串,展示时直接WebView加载,天然统一。
3. 实操过程与核心环节实现
3.1 搭建数据库:从实体类到建表SQL的快捷路径
先建数据库和表还是先写Java实体类?根据我的经验,推荐先画好实体类,再用MyBatis-Plus的插件直接生成建表SQL。很多同学不知道,IDEA插件市场里有一个“MyBatis-Plus SQL Generator”之类的工具,你根据Java实体类就能自动生成创建表的SQL语句,字段类型、长度、注释都能从注解里读取到。
比如这个User实体类:
@Data @TableName("user") public class User { @TableId(type = IdType.AUTO) private Integer id; private String username; private String password; private String nickname; private String avatar; private String phone; /** 0-普通用户 1-管理员 */ private Integer role; private Date createTime; }通过这个实体类生成的SQL基本长这样:
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) DEFAULT NULL, `password` varchar(100) DEFAULT NULL, `nickname` varchar(50) DEFAULT NULL, `avatar` varchar(255) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `role` int(11) DEFAULT '0', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;注意字符集一定要用utf8mb4,不要用utf8,因为utf8在MySQL里存不了emoji表情,评论内容但凡出现一个emoji,整个插入操作就会报错。
生成的SQL还不够,表之间要设置外键还是不加外键?我的建议是:不加物理外键,只建立逻辑关联。物理外键虽然在数据库层面保证了数据的完整性,但会导致删除用户时受到关联限制、插入性能下降等问题,而且MyBatis-Plus的关联查询根本不会用到外键约束。更重要的是,答辩时老师如果问“为什么不用外键”,你可以理直气壮地解释:现代互联网应用为了追求性能和扩展性,普遍采用应用层维护数据关联的方式,逻辑外键更灵活。
所有表建好之后,插入一部分测试数据时要注意,景点的图片地址别写死成本地的,用几个真实的外链图片URL,这样Android端跑起来界面好看,演示效果会好很多。
3.2 后端接口开发:三层架构的一个完整实例
我拿景点分页查询接口来讲完整流程。
Controller层:
@RestController @RequestMapping("/scenic") public class ScenicController { @Autowired private ScenicService scenicService; @GetMapping("/page") public ResultVO getScenicPage(@RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String keyword, @RequestParam(required = false) Integer categoryId) { return ResultVO.success(scenicService.getScenicPage(current, size, keyword, categoryId)); } }Service层:
@Service public class ScenicServiceImpl extends ServiceImpl<ScenicMapper, Scenic> implements ScenicService { @Override public PageResult getScenicPage(Integer current, Integer size, String keyword, Integer categoryId) { QueryWrapper<Scenic> wrapper = new QueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(keyword), "name", keyword) .eq(categoryId != null, "category_id", categoryId) .orderByDesc("create_time"); Page<Scenic> page = scenicMapper.selectPage(new Page<>(current, size), wrapper); // 把MyBatis-Plus的Page对象转换成自定义的PageResult返回 return new PageResult(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize()); } }整个过程遵循“Controller只管接收参数和返回结果、Service管业务逻辑、Mapper管数据查询”,每层各司其职。评审老师看到这种清晰的代码结构,印象分通常会拉满。
核心API清单如下:
| 接口路径 | 请求方式 | 功能说明 | 是否需要登录 |
|---|---|---|---|
| /user/register | POST | 用户注册 | 否 |
| /user/login | POST | 用户登录 | 否 |
| /scenic/page | GET | 景点分页列表 | 否 |
| /scenic/{id} | GET | 景点详情 | 否 |
| /article/page | GET | 攻略列表 | 否 |
| /article/{id} | GET | 攻略详情含评论 | 否 |
| /article/add | POST | 发布攻略 | 是 |
| /comment/add | POST | 发表评论 | 是 |
| /favorite/add | POST | 收藏景点 | 是 |
| /favorite/list | GET | 我的收藏列表 | 是 |
| /admin/scenic/save | POST | 景点新增/编辑 | 是(管理员) |
| /admin/article/audit | POST | 攻略审核 | 是(管理员) |
3.3 Android页面实现:从登录到列表的一套完整流程
Android端的核心页面主要就是六个:登录页、注册页、首页列表、景点详情、攻略详情、个人中心。
登录页的核心代码就是一个Retrofit请求:
public void login(String username, String password) { ApiService api = RetrofitManager.getInstance().getApi(); LoginRequest request = new LoginRequest(username, password); api.login(request).enqueue(new Callback<ResultVO<User>>() { @Override public void onResponse(Call<ResultVO<User>> call, Response<ResultVO<User>> response) { if (response.body() != null && response.body().getCode() == 200) { User user = response.body().getData(); // 保存用户信息到SharedPreferences PrefManager.saveUser(user); PrefManager.saveToken(user.getToken()); // 跳转到首页 startActivity(new Intent(LoginActivity.this, MainActivity.class)); finish(); } else { Toast.makeText(LoginActivity.this, "登录失败", Toast.LENGTH_SHORT).show(); } } @Override public void onFailure(Call<ResultVO<User>> call, Throwable t) { Toast.makeText(LoginActivity.this, "网络连接失败", Toast.LENGTH_SHORT).show(); } }); }首页列表是一个典型的RecyclerView场景。布局是垂直滚动,每个item展示景点缩略图、名称、评分、简要描述。点击item跳转到详情页,详情页在onCreate里根据传入的id请求接口并渲染。
列表分页我用的方案是“下拉刷新 + 上拉加载更多”,下拉用Android官方SwipeRefreshLayout,上拉通过判断RecyclerView滚动到末尾时自动加载下一页:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager(); if (lm != null && lm.findLastVisibleItemPosition() >= lm.getItemCount() - 2) { // 加载更多 loadMore(); } } });注意这里有个小坑:loadMore函数里要加一个isLoadingMore标志位,防止滑动时多次触发重复请求。这个标志位在请求开始时置为true,请求完成后置为false。
3.4 后台管理的实现思路
后台管理页面我用了Thymeleaf + Bootstrap。Bootstrap是前端CSS框架,但用法很傻瓜,直接CDN引入,就能用现成的表格、按钮样式,不用自己写样式。
后台管理的关键是权限控制。我在后端写了一个拦截器(HandlerInterceptor),在进入管理端所有接口之前,检查请求头里的token对应的用户角色是否为管理员:
public class AdminInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token"); User user = userService.getUserByToken(token); if (user == null || user.getRole() != 1) { response.setStatus(401); return false; } return true; } }这个拦截器的注册要写进WebMvcConfigurer实现类中。这里需要注意排除登录接口本身,并且把静态资源目录(css、js)放行,否则页面的样式全挂掉,看起来像没做前端一样。
4. 常见问题与排查技巧实录
做毕设一定会踩坑,关键是踩完坑知道怎么爬出来。下面这些是历届学生和我自己实际遇到的高频问题,整理成表格方便对照排查。
| 问题现象 | 根本原因 | 排查方法 | 解决方式 |
|---|---|---|---|
| Spring Boot启动失败,报错说端口被占用 | 8080端口被其他程序占用 | 命令行执行netstat -ano找到占用进程 | 关闭占用程序,或改application.yml端口 |
| 数据库连接失败 | 时区配置错误、MySQL未启动、账号密码不符 | 检查MySQL服务状态,控制台直接登录验证 | 连接串加serverTimezone=Asia/Shanghai |
| Entity类报错“Invalid bound statement” | Mapper接口和XML文件没有映射到 | 检查mapper-locations配置 | 在application.yml中配置mybatis-plus.mapper-locations |
| Android连不上后端接口 | 模拟器地址写错 | 检查基础URL配置 | 模拟器访问宿主机必须用10.0.2.2,不能用localhost |
| App闪退,日志一堆 | 空指针是最大的可能 | 查看Logcat红色异常信息 | 找到报错行,检查对象是否初始化 |
| 中文乱码 | 字符集不一致 | 检查数据库、连接串、前端页面编码 | 统一使用utf8mb4;URL加characterEncoding=utf8 |
| 图片上传后无法显示 | 静态资源映射未配置 | 后端访问图片地址是否404 | 实现WebMvcConfigurer映射/upload/路径到本地目录 |
| 分页查询返回总数为0 | 数据量统计异常,或wrapper条件错误 | 打印SQL到控制台检查 | 开启MyBatis-Plus日志,看排除条件 |
| 评论后列表不刷新 | 未重新请求详情接口 | 检查是否在onResume里刷新数据 | 页面返回时重新加载数据 |
| 安卓控件点击没反应 | 控件设置了不可点击属性,或被遮挡 | 检查布局层级 | 调整控件z轴顺序,检查clickable属性 |
这里我单独展开几个最有代表性的问题,因为这些坑看起来很小,却能卡住你好几个晚上。
第一个是“模拟器访问localhost失败”。这个问题几乎每个月都有人来问。你的Spring Boot跑在自己电脑上,Android模拟器是虚拟机,模拟器里的localhost是模拟器自己,不是你电脑。你必须写http://10.0.2.2:8080,这是Android官方规定的模拟器访问宿主机的专用地址。如果是真机调试,则要写你电脑在局域网里的IP,手机和电脑连同一个WiFi。这个知识点必须记牢,换了环境就报网络连接失败,十有八九是这个问题。
第二个是Spring Boot版本太高导致的依赖冲突。我的项目用Spring Boot 2.7.x,配套的MyBatis-Plus版本应该选3.5.x,JDK用8或11。如果你直接启动一个新项目选了Spring Boot 3.2.x,那么JDK要17、MyBatis-Plus要升到3.5.5以上,而且很多东西的API有变化,网上大部分教程代码都跑不了。所以建项目时宁可选低一档的版本,稳定压倒一切。新版特性带来的复杂度对毕设来说根本用不上。
第三个是Gradle项目构建缓慢或失败。Android Studio现在默认用Gradle构建,由于网络原因,Gradle首次下载可能特别慢。解决办法是在项目根目录的build.gradle文件里配置阿里云的Maven镜像仓库:
buildscript { repositories { maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/google' } mavenCentral() google() } }同时在gradle-wrapper.properties中把Gradle版本和本地已下载的版本保持一致,如果本机有现成的,就不要重新下载。
第四个是MyBatis-Plus查询结果字段全是null。这个极大概率是map-underscore-to-camel-case没配置,或者实体类没有加@TableName注解,数据表表名和实体类名对不上。比如实体类叫ScenicInfo,表名是scenic,MyBatis-Plus默认会把ScenicInfo映射到scenic_info表,于是查询报错。
第五个是评论功能的时间显示不对。数据库里存的是带时区的datetime,但Java的Date默认使用本地时区,如果配置不一致就可能导致时间相差8小时。解决办法是在创建实体类时的createTime字段上加上@TableField(fill = FieldFill.INSERT),并用一个MetaObjectHandler处理器统一填充创建时间:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", Date.class, new Date()); } }5. 文档、视频与答辩准备的实用建议
源码、文档、运行视频、讲解视频这四样东西,是很多同学买毕设项目时比较在意的交付物。其实这几样东西的本质是“帮你消化这个项目”,不是“买了就完事”。文档尤其重要,因为答辩打分很大比重看文档的规范程度。
我的项目文档里包含的章节可以给你参考:
- 需求分析:项目背景、功能需求概述、可行性分析
- 系统设计:总体架构图、功能模块图、数据库ER图、表结构说明
- 系统实现:每个模块的核心代码说明、关键逻辑截图
- 系统测试:测试用例表、测试结果、缺陷记录
- 总结与展望:遇到的困难、解决方案、后续改进方向
写文档有个诀窍:先写表结构说明和接口文档,再补需求分析,最后写测试报告。因为表结构是固定的,接口是实测跑通的,这两部分不会虚;需求分析可以反过来从表结构反推;测试报告用你自己操作过程中的截图,绝对真实可信。
运行视频录制时,我建议在录制前先列一份“演示脚本”,按照功能点顺序操作:登录注册→首页浏览→景点详情→搜索→查看攻略→发表评论→个人中心→后台管理端操作。每一步操作3到5秒,语速平缓。视频的作用是让导师快速了解系统的全貌,不需要炫技,但要流畅、无卡壳。如果操作到一半发现Bug了,宁可重新录一条,也不要剪接,导师最忌讳看到“演示完功能就报错”的画面。
讲解视频是给期末答辩用的“提前预习”。录制时不用面面俱到,重点讲三个部分:项目背景和功能概览、数据库表结构和关联关系、核心功能的技术实现。具体到代码层面,可以挑登录鉴权、分页查询、上传图片这三个点详细讲,因为这三个点能同时体现你的后端理解、数据库能力和工程细节把控。
答辩现场还有一个容易被问倒的问题:“如果数据量变大了,你的系统怎么优化?”这个问题不要慌,哪怕是毕设系统,你也可以从技术角度给出方案:分页查询已经做了可以减小单次查询压力;给表增加索引(比如搜索字段建普通索引);热点数据可以用Redis做缓存。你要表达的是“我理解这个方向,并且知道业界成熟方案”,而不是一定要在代码里实现Redis。设计时的可扩展性远比你现场临时堆功能更重要。
6. 关于项目后续的扩展思路
如果做完这个系统还有余力,或者想让项目在简历上更有亮点,有两条改动成本低但加分明显的路径。
第一条是给“攻略模块”增加标签体系和推荐逻辑。每篇攻略打几个标签,比如“穷游”、“亲子”、“美食”、“徒步”,然后用简单的规则推荐同类攻略。不需要机器学习算法,一个简单的关联查询就能做出“猜你喜欢”的效果。在简历上写“基于标签关联的内容推荐”就比单纯“攻略展示”更有说头。
第二条是把管理端的图片上传从本地存储改成云存储。现在的云对象存储服务提供了对应SDK,接入复杂度很低。在这个改动过程中,你实际接触到的是生产级别的文件存储方案,这个技能面试时会比较讨喜。
我个人大量实操之后有一个明显体会:毕业设计项目的价值不在于用了多高深的技术,而在于你是否把每个选择背后的逻辑想清楚了。Spring Boot为什么用这个版本、MyBatis-Plus为什么能省掉大量手写SQL、Android端为什么用Retrofit不用原生HttpURLConnection,这些“为什么”才是答辩时你真正能说出来的东西,也是以后面试官真正会追问的东西。照着这篇文章的路线做下来,你收获的不止是一个能交差的系统,还有一套完整的全栈开发思路。