作为从大二就开始用SpringBoot接外包的老人,我见过太多把项目做成“启动类 + Controller + 贫血Service”的玩法。这次帮人完整做了一套美食分享平台,从数据库设计到部署上线走了一遍,想把它沉淀成一篇能直接抄作业的记录。基于SpringBoot的美食分享平台,本质上是一个典型的社区内容系统:用户注册登录、发布带图片的美食帖子、其他用户点赞评论关注,平台按菜系和关键词搜索内容。相比常见的学生管理系统,它真正有含金量的地方在于文件上传、互动计数和内容检索,这也是面试官最愿意深挖的点。
整个项目我用了三周左右,前端选了Vue3 + Element Plus,后端用SpringBoot提供RESTful接口,数据库用MySQL,缓存按需上Redis。下面不按教科书那种“需求分析→概要设计→详细设计”的顺序来写,而是把我在实际编码过程中最关键的技术决策、版本选型和踩坑记录串起来,给正在做毕业设计、或者想拿全栈项目练手的人一个可参照的模板。
1. 项目整体设计与技术选型
1.1 为什么用SpringBoot而不是SSH或SSM
先说一个很多人不太愿意面对的事实:现在2025年,SSH早就没人讨论了,SSM也基本退到老项目维护的场景里。我大四实习时维护过一套SSM的电商后台,光spring-mvc.xml、mybatis-config.xml、web.xml加起来就几百行,还要手工处理各种jar包冲突,部署时把war扔到Tomcat里等半天启动。SpringBoot通过starter和自动配置把“配置”变成了“约定”。比如数据源,只要引入spring-boot-starter-jdbc,再写上数据库url、用户名和密码,它就能直接创建DataSource,省掉一整个xml文件。
这里要特别说一个知识点:springboot自定义自动配置。很多人会用starter,但不知道自动配置的原理。简单讲,SpringBoot启动时会扫描当前classpath下的spring.factories或AutoConfiguration.imports文件,把里面声明的配置类加载进来,再配合@ConditionalOnClass、@ConditionalOnMissingBean这些条件注解决定要不要生效。这也是为什么你引入redis-starter后,不写任何配置类也能直接用RedisTemplate——因为有Spring Boot帮你在启动时自动配置了它。理解这个机制,以后想封装自己的公共模块、写公司内部starter,思路就是现成的。
版本选择上面,我踩了一个所有人都可能踩的坑:SpringBoot版本不是越高越好。最开始我图新鲜用了SpringBoot 3.2,结果JDK必须先升到17,MyBatis-Plus还得换成spring-boot3专用依赖,更麻烦的是javax.servlet包全部变成了jakarta.servlet,网上搜到的老教程代码直接编译不过。后来我退回2.7.18,JDK8就能跑,各种第三方兼容性最稳。如果你是做课程设计或者毕业设计,没有硬性要求就别追求最新版本,2.7.x是目前最舒服的区间。
1.2 技术栈选型与整体目录结构
这套项目用到的技术栈,我整理成了一张表,方便你直接参考:
| 层次 | 技术选型 | 选择理由 |
|---|---|---|
| 后端框架 | SpringBoot 2.7.18 | 稳定、资料多、JDK8兼容 |
| 数据访问 | MyBatis-Plus 3.5.3 | 单表CRUD不用手写SQL,分页插件成熟 |
| 数据库 | MySQL 8.0 | 主流关系型数据库,功能验证方便 |
| 缓存 | Redis 5+ | 验证码、热点数据缓存,也可存点赞状态 |
| 认证鉴权 | JWT + 拦截器 | 无状态认证,前后端分离友好 |
| 文件存储 | 本地磁盘 + 静态资源映射 | 毕设级别够用,不增加额外成本 |
| 前端 | Vue3 + Element Plus + Vite | 组件丰富,开发效率高 |
我见过不少同学用SSM或者SpringMVC的老项目改造,结果前端换成Vue3后,跨域问题、session共享问题层出不穷。SpringBoot天然适合RESTful风格接口,返回JSON太方便了,这也是选它的一个重要原因。
再讲项目结构。很多人一上来就controller/service/mapper三层打天下,小项目还好,一旦功能多了就乱。我的建议是加上entity、dto、common、config这些包,职责分清楚。实际目录大概长这样:
com.example.foodshare ├── controller # 接口层,只做参数接收和返回 ├── service # 业务逻辑层,事务、业务规则放这里 ├── mapper # MyBatis-Plus的数据访问接口 ├── entity # 和数据库表字段一一对应的实体 ├── dto # 前端交互的数据对象,避免暴露实体 ├── common # 统一返回结构、全局异常、常量 ├── config # 跨域配置、静态资源映射、拦截器注册 ├── interceptor # JWT登录拦截器 └── utils # JwtUtil、FileUtil等工具干嘛要单独搞一套dto?因为数据库实体里往往有password这类敏感字段,直接序列化返回给前端就是事故。把实体和返回对象分离,既能保护字段,又能根据页面需要灵活组合数据。这个习惯越早建立越好,后面面试聊项目时也能说出点门道。
1.3 数据库设计与核心表结构
数据库设计决定了这个项目的天花板。美食分享平台最少需要七张表:用户表、帖子表、图片表、评论表、点赞表、关注表、分类表。
用户表user:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| username | varchar(50) | 登录账号,唯一索引 |
| password | varchar(100) | BCrypt加密后的密码 |
| nickname | varchar(50) | 昵称 |
| avatar | varchar(255) | 头像地址 |
| city | varchar(50) | 城市,便于同城美食推荐 |
| create_time | datetime | 注册时间 |
帖子表food_post:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint | 发布人 |
| category_id | int | 菜品分类 |
| title | varchar(100) | 标题 |
| content | text | 美食描述 |
| cover_image | varchar(255) | 封面图 |
| like_count | int | 点赞数,冗余字段 |
| comment_count | int | 评论数,冗余字段 |
| status | tinyint | 0草稿 1已发布 2下架 |
| create_time | datetime | 发布时间 |
图片表post_image:id、post_id、image_url、sort_order,用来存一个帖子多张图对应的地址,排序字段控制展示顺序。
评论表comment:id、post_id、user_id、content、parent_id、create_time。parent_id支持楼中楼,简单场景解析成两层就够了。
点赞表like_record:id、user_id、post_id、create_time。这里必须加联合唯一索引uk_user_post(user_id, post_id),保证一个人不能对同一个帖子重复点赞。
关注表user_follow:id、user_id、follow_user_id、create_time,同样加唯一索引,防止重复关注。
分类表category:id、name、sort_order,比如火锅、烧烤、甜品、家常菜。
为什么要把like_count和comment_count冗余在帖子里?因为列表页和详情页要高频展示这两个数字。如果每次都去count点赞表,数据量一上来,页面会明显变慢。冗余字段会增加一点维护成本,但通过事务和原子更新SQL可以保证最终一致。这个设计思路面试时非常加分,属于“会用索引和反范式设计”的体现。
2. 核心功能模块设计与实现
2.1 用户注册登录与保存登录态
注册流程看起来简单,里面有两个必须处理的点:密码加密和用户名唯一性检查。密码绝不能用MD5直接存,现在GPU暴力破解MD5速度太快,应该用BCrypt加密。Spring Security里有一个现成的BCryptPasswordEncoder,即使不引入完整Spring Security,也可以单独引一个spring-security-crypto包来用。登录时用bcrypt.matches(rawPassword, encodedPassword)校验,不需要解密,因为BCrypt本身是不可逆的。
登录成功后的登录态保存,我选了JWT。JWT的核心原理是服务端不保存session,把一个包含用户id、用户名和过期时间的token签发给客户端,客户端之后每次请求都带上这个token,服务端验签后就能确认身份。对前后端分离场景相当友好,后端多个实例部署时也不需要共享session。
代码实现并不复杂,核心就是生成token和校验token:
// JwtUtil的核心方法 public String generateToken(Long userId, String username) { Map<String, Object> claims = new HashMap<>(); claims.put("userId", userId); claims.put("username", username); return Jwts.builder() .setClaims(claims) .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000L)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }然后写一个拦截器,对需要登录的接口做token校验。我用自定义注解@RequireLogin标记接口,拦截器里判断handler是否标注了这个注解,如果有就取Header中的token并解析,解析成功就把userId放入ThreadLocal,供后续业务逻辑直接获取。这个方式比在Controller里每个方法都传一个token参数干净得多。
2.2 美食帖子发布与图片上传
发布帖子的前端表单包含标题、内容、分类、封面图、多张展示图。后端接收时,用@RequestPart("files") List<MultipartFile> files接收文件列表,同时用@RequestParam接收其他表单字段。
这里的事务控制要特别注意:必须先保存food_post主记录拿到自增id,然后再循环保存post_image图片关联记录,这整个过程必须在同一个事务里。如果帖子保存成功,图片记录插入时报错,整个方法要回滚,否则会出现一个没有图片的空壳帖子。
图片上传其实是个容易翻车的地方。我不建议直接存到数据库BLOB字段,也先不建议一上来就接阿里云OSS,除非你确实有钱有券。最稳妥的做法是存在本地磁盘指定目录,文件名用UUID重命名,后缀保留原文件的扩展名。这样做有两个好处:一是避免上传同名文件互相覆盖,二是避免文件名里有中文或特殊字符导致访问路径解析出错。
2.3 点赞、评论与关注互动
这些互动功能不要想复杂,靠数据库约束和原子操作就能做得可靠。
点赞功能我用的是“先插入、后计数”的策略。like_record表有(user_id, post_id)联合唯一索引,用户点一次赞就执行一次INSERT,如果重复点赞,数据库会直接报DuplicateEntry异常,我们捕获这个异常就知道他已经点过了。点赞数不用先查后改,而是执行一条原子SQL:
UPDATE food_post SET like_count = like_count + 1 WHERE id = #{postId}这样在高并发下也不会把数字改错。很多新手喜欢先select出来,在Java里加1,再update回去,两个请求同时进来就会丢更新。这个知识点面试时也经常出现,属于必须掌握的基础。
评论表用parent_id支持回复。如果parentId为null就表示顶级评论,如果有值就表示回复某一条评论。做前端展示时,只需要查顶层评论,再按parentId批量查出子评论,组装成一棵两层树。这个方案对美食分享的评论场景完全够用,不必上重型的评论组件。
关注功能本质上是记录一个“谁关注了谁”的关系。用户点关注时,先查user_follow表有没有记录,没有就插入,同时更新对方的粉丝数;取消关注则删除记录并减掉粉丝数。这里同样依靠唯一索引保证不会出现一条重复关注记录。
2.4 搜索与推荐功能的实用做法
搜索别一上来就上Elasticsearch,那是给自己找罪受。你做一个毕设,数据量撑死几万条,用MySQL的LIKE模糊查询就够了。核心代码写在Mapper里:
@Select("SELECT * FROM food_post WHERE status = 1 AND (title LIKE CONCAT('%', #{keyword}, '%') OR content LIKE CONCAT('%', #{keyword}, '%'))") List<FoodPost> searchPosts(String keyword);配合分类条件的where和分页插件,就能实现“按关键词+按分类+分页”的搜索效果。唯一的坑是模糊查询以通配符开头时MySQL会放弃索引,但这在数据量小的项目里影响不大,不要为了优化而过度设计。
推荐功能不要做成用户画像推荐系统,那是算法工程师的活。美食平台的合理推荐方式很简单:给你列表页加一个“热门排序”。热度我用的公式是:
score = like_count * 1 + comment_count * 2 + (create_time_unix / 3600)解释一下:评论的互动深度比点赞高,所以权重设为2;create_time_unix除以3600是为了让新发布的帖子在分数上享有一点时间优势,不然老帖子永远霸榜。这个公式看起来朴素,但实际效果比单纯按时间倒序好很多。如果你还想更动态一点,可以用SpringBoot的定时任务@Scheduled,每天凌晨算一次热度并更新到帖子的score字段,查询时直接order by score。
3. 实操过程与关键代码实现
3.1 项目搭建与常用依赖配置
我直接通过IDEA的Spring Initializr创建项目,选Java 8和SpringBoot 2.7.18。如果你对maven比较熟,也可以直接在pom.xml里自己配。下面这几个依赖是美食分享平台的核心:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> </dependencies>这里有个版本匹配的经验:SpringBoot 2.7.x对应的mysql-connector-java版本由parent自动管理,不需要自己写版本号;而mybatis-plus-boot-starter 3.5.x同时兼容SpringBoot 2.x和3.x,但3.x要加额外的适配包。我为什么反复强调版本,因为真正拖垮你三天进度的往往不是业务逻辑,而是依赖冲突。
application.yml里的核心配置我也放在下面:
spring: datasource: url: jdbc:mysql://localhost:3306/food_share?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword servlet: multipart: max-file-size: 10MB max-request-size: 30MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true把log-impl设为StdOutImpl,开发时能在控制台直接看到生成的SQL,排查数据访问问题特别有用。map-underscore-to-camel-case让数据库的create_time自动映射成Java的createTime,省去一堆ResultMap。
3.2 登录拦截器与全局用户信息注入
我之前说过用自定义注解+拦截器,这里给你看最关键的实现。
先定义一个注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireLogin { }矛拦截器核心代码:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod method = (HandlerMethod) handler; RequireLogin requireLogin = method.getMethodAnnotation(RequireLogin.class); if (requireLogin == null) { return true; } String token = request.getHeader("Authorization"); if (!StringUtils.hasText(token)) { throw new BusinessException(401, "未登录"); } Claims claims = JwtUtil.parseToken(token); UserContext.set(claims.get("userId", Long.class), claims.get("username", String.class)); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }UserContext本质是一个ThreadLocal,存当前请求的用户id。这样在service层直接UserContext.getUserId()就能知道是谁在操作,不需要在接口方法里一次次传userId。注意必须在afterCompletion清理ThreadLocal,否则线程池复用时数据会串。
注册拦截器也很简单:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/api/auth/login", "/api/auth/register", "/uploads/**", "/error"); } }哪些接口需要登录,由方法上的@RequireLogin决定,而不是在路径匹配里写死,这样灵活很多。
3.3 图片上传与静态资源路径映射
图片上传的文件处理代码,我把完整流程贴出来:
public String storeFile(MultipartFile file) throws IOException { String originalFilename = file.getOriginalFilename(); String extension = StringUtils.getFilenameExtension(originalFilename); String newFilename = UUID.randomUUID().toString().replace("-", "") + "." + extension; Path uploadPath = Paths.get(uploadDir).toAbsolutePath().normalize(); Files.createDirectories(uploadPath); Path targetPath = uploadPath.resolve(newFilename); file.transferTo(targetPath); return "/uploads/" + newFilename; }这里一定要用Paths.get(uploadDir).toAbsolutePath().normalize(),把路径里的..和符号处理掉,否则用户构造特殊文件名时可能造成路径穿越,这是安全上必须注意的。
保存文件后,还要让SpringBoot能把/uploads/开头的URL映射到本地目录。写一个静态资源配置:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String location = "file:" + uploadDir + "/"; registry.addResourceHandler("/uploads/**").addResourceLocations(location); }注意Windows下路径可能类似E:/upload/,要在配置里写成带盘符的绝对路径,Linux则是/home/app/upload/。配置不对时浏览器访问图片会404,而且是那种明明文件存在却访问不到的404,排查起来很恼火。
还有一个容易踩的坑:SpringBoot默认单文件最大是1MB。美食帖子图片动辄几MB,不调配置就会抛异常:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 30MB我做项目时把单文件限到10MB,一次请求总大小限到30MB,这样一次上传多图时不会卡的太死。
3.4 统一返回结构、异常处理与前端联调细节
前后端对接最烦的就是返回格式不统一。一会儿直接返回对象,一会儿返回Map,前端根本没法写拦截器。我在这套项目里统一用Result 包装:
public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.message = "ok"; r.data = data; return r; } public static <T> Result<T> error(Integer code, String message) { Result<T> r = new Result<>(); r.code = code; r.message = message; return r; } }配合@RestControllerAdvice做全局异常捕获,把所有业务异常、校验异常、上传异常统一转成Result返回。这样前端在axios响应拦截器里只需要判断code是否等于200,其他所有情况都走全局错误提示组件,不需要每个页面都写重复的try-catch。
前端联调时还要处理跨域。Vue3开发服务器默认在5173端口,后端在8080端口,怎么都算跨域。我在后端加一个CorsFilter:
@Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/**", config); return new CorsFilter(source); }记住生产环境里别用allowedOriginPattern("*")配合credentials,有安全隐患,开发阶段图省事可以这么干。
4. 常见问题与排查技巧实录
4.1 SpringBoot版本过高导致的依赖迁移问题
前面已经说了我在3.2上的遭遇。最典型的问题是,网上绝大多数的SpringBoot教学视频、老教程都默认使用javax.servlet和javax.validation包。而SpringBoot 3.0开始,整个Java EE标准被迁移到Jakarta命名空间,所有import javax.*的代码全部编译报错。你还要把JDK升到17,但很多学校的毕设环境还是JDK8,这就非常被动。
| 对比项 | SpringBoot 2.7.x | SpringBoot 3.2.x |
|---|---|---|
| JDK版本 | 8及以上 | 17及以上 |
| 包名 | javax.* | jakarta.* |
| MyBatis-Plus | 普通starter | 需要spring-boot3专用starter |
| 第三方兼容 | 大量成熟方案 | 新库兼容,老库可能要换版 |
我的建议很直接:毕业设计和课程设计首选2.7.x,除非你的毕业设计题目明确要求用新版本。实际工作里公司系统也不会整天升大版本,稳定压倒一切。
4.2 图片上传失败和访问404的排查方向
我在做这个项目时,把图片上传的问题按现象拆解过:
如果上传文件报SizeLimitExceededException,那就是multipart配置没改,我上面那段配置抄上去就行。
如果文件保存成功,但前端访问/uploads/xxx.jpg返回404,优先检查静态资源映射目录和实际存储目录是否一致。尤其要注意映射的目录最后有没有斜杠,以及uploadDir是相对路径还是绝对路径。最傻的一次是我把file:后面的路径写成了uploadDir变量,但uploadDir是相对路径“upload”,配置文件放在项目根目录运行时有项目根路径兜着还好,打成jar包部署后就完全找不到目录了。最后我把路径配置改成绝对路径才算彻底解决。
还有一个不算坑但很常见的需求:如果想在上传后生成缩略图,比如列表页用压缩图,建议参考thumbnailator这类轻量库。我因为在detail和列表共用同一张原图,导致列表页图片加载明显变慢,后来干脆给列表页返回封面图地址,详情页才加载完整图集。
4.3 MyBatis-Plus字段自动填充失效和分页插件
很多同学用MyBatis-Plus时遇到一个问题:@TableField(fill = FieldFill.INSERT)在insert时没有自动填充创建时间。这通常是缺少MetaObjectHandler实现。我写了一个公共的字段填充器:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }还有一个问题:想用MyBatis-Plus的分页查询,需要显式加一个分页插件Bean:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }不加这个Bean,你调selectPage时得到的记录数是0,或者分页参数不生效,这是踩过的人最多的坑。
4.4 并发点赞和数据一致性处理
真实场景里两个人同时点赞,如果代码是先select再update,极可能造成点赞数丢失一次。我的方案已经讲过:唯一的联合索引约束 + 原子UPDATE。但值得一提的是,当用户取消点赞时,也要用原子的like_count = like_count - 1,并且要做下限控制,避免减成负数。SQL可以写成:
UPDATE food_post SET like_count = GREATEST(like_count - 1, 0) WHERE id = #{postId}如果你后续想用Redis来扛更高的并发,点赞状态可以写进Redis的set,点赞数用incr/decr命令,再通过定时任务异步刷回MySQL。但在此之前,先把纯MySQL方案做到正确。很多同学的“高并发设计”只是嘴上说说,真实项目里先把唯一索引和原子更新用明白,已经超过了很大一部分人。
5. 部署上线与后续扩展建议
5.1 项目打包与部署方式
后端代码写完后,用Maven打包:
mvn clean package -DskipTests打完包会在target目录下生成foodshare-0.0.1-SNAPSHOT.jar。直接上传到服务器,用java命令启动:
nohup java -jar foodshare-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > app.log 2>&1 &生产环境的数据库地址、上传目录这些配置,我建议通过application-prod.yml单独维护,里面写服务器对应的MySQL连接和绝对路径。不要把本地开发配置和生产混在一起,否则上线第一个晚上就在改数据源。
如果你想用宝塔面板这类工具部署,操作流程也很简单:先在软件商店安装MySQL和JDK,然后上传jar包,在网站页面里添加一个Java项目,指定jar路径和端口,再把MySQL数据库数据导入进去。如果遇到外部访问不了,检查一下防火墙是否放行了对应端口。这套流程对没有运维基础的同学非常友好。
考虑到以后可能会有多环境复制需求,也可以写一个简单的Dockerfile:
FROM openjdk:8-jdk-slim COPY target/foodshare-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT ["java", "-jar", "/app.jar"]Docker的好处是部署时只需要build一个镜像,所有依赖环境都封装在里面,换服务器不需要重新配JDK。
前端打包用npm run build,把生成的dist目录放到Nginx的静态目录里,再把/api开头的请求反向代理到后端的8080端口。这是目前最常见的前后端分离部署方式,比把前端手动扔进SpringBoot的static目录要干净得多。
5.2 后续可以扩展的方向
功能上建议留好口子,但不建议一开始全做完。合理路线是先保证“发帖-浏览-互动”这个主链路完整稳定,再考虑下面这些扩展方向。
第一个是引入OSS对象存储。本地磁盘方案在部署到云服务器时有两个硬伤:磁盘空间有限,以及如果应用扩容到多台服务器,文件不在同一台机器上就会访问不到。换成阿里云OSS或腾讯云COS后,上传接口只需拿到文件流然后直传云端,返回的是云上的URL,性能和维护体验都好很多。第二个是把Redis用起来。除了登录验证码,还可以把热门帖子的详情缓存到Redis,给列表接口做缓存,减少数据库压力。第三个是消息队列。比如用户关注后、帖子被点赞后,可以发MQ消息通知对方,不过这个对毕设来说有点重,属于锦上添花。第四个是写一个小程序端。后端RESTful接口天然适配小程序,前端复用经度,整个项目的复杂度会提升一个档次。
5.3 实用小技巧:把调试过程做得更顺
最后分享一个我从这个项目里总结出的经验:开发阶段一定要把SQL日志打开。很多人排查问题只会断点调试,但大量和数据相关的问题,只要看到MyBatis打印出来的SQL,一秒就能定位是参数问题还是SQL写法问题。MyBatis-Plus的log-impl配成StdOutImpl就是干这个用的。另外,前端联调时在浏览器Network里先把请求和响应结构固定好,不要后端改一个字段前端改一个字段,那样永远联调不完。接口文档可以先不写得很重,但要保证每个接口的返回字段命名是稳定的,比如时间统一用String类型返回,前端不用单独转换格式。
这套项目我做下来最大的感受是:技术选型不需要炫技,把SpringBoot的数据访问、文件上传、认证鉴权这些基本功磨扎实,比堆几个花哨的组件更有价值。你如果正准备做类似题目,不妨先按这个路径把主流程走通,再根据自己情况往上面加细节。等把数据访问、文件处理、权限控制这几个环节都亲手调过一遍,你对SpringBoot的理解肯定就不只是会打一个@RestController注解了。