Spring Boot美食分享平台实战:技术选型、数据库设计与部署优化

发布时间:2026/10/9 10:54:43
Spring Boot美食分享平台实战:技术选型、数据库设计与部署优化
1. 美食分享平台项目为什么我会选 Spring Boot 来做聊到个人博客、内容社区这类项目我见过太多人一上来就选很重的方案微服务先拆四个服务、数据库直接上分库分表、消息队列先挂上。结果往往是开发周期拖到三个月连用户登录都还没做完。这次做美食分享平台我一开始就定了个调子项目定位是社区型内容平台不是互联网大厂的高并发系统单体 Spring Boot 就是最务实的答案。当初这个项目立项时需求其实非常明确用户能发图文混排的美食菜谱能按分类找菜能收藏、点赞、评论最好还能关注喜欢的美食作者。说实话这些功能听起来不少但仔细拆开之后都是经典的 CRUD 关联查询 排序分页没有任何一个需要分布式事务或者大数据量处理。用微服务架构去做这种项目等于拿航空母舰去跑小区门口的快递三轮车线路治理成本比业务本身还高。Spring Boot 在这里的价值不只是约定优于配置那几个字。它真正帮我省时间的地方在于自动配置机制引入spring-boot-starter-web就拿到了内嵌 Tomcat 和完整的 MVC 能力引入spring-boot-starter-data-jpa就有了数据访问层我只需要写业务代码不用花两天去拼 ssh 时代的 XML 配置文件。起步依赖Redis、MyBatis、验证框架、Swagger全都是一行依赖的事版本冲突也由 Spring Boot 的 BOM 统一管理。生态成熟菜谱图片上传、全文搜索、定时任务这些需求在 Spring Boot 生态里都有非常成熟的落地方案不用自己造轮子。部署友好最终打出一个可执行 jar 包扔到服务器上就能跑后续如果要上 Docker 也很顺。搜springboot相关热词的时候我看到很多人在纠结 Spring Boot 版本选取、项目结构规范这些东西。我的经验是这类内容型项目别选太新的版本选稳定版。当时我用的 Spring Boot 2.7.x不是最新的但它兼容性最好遇到坑时网上的解决方案最多。等我把核心功能全部跑通、部署上线、压测完成之后再去考虑升不升级这才是合理的节奏。下面我把整个项目的解剖过程写出来从功能规划到表设计从前端接口到部署上线全部是真实跑过的路径适合也想做类似内容社区项目的同学参考。2. 平台功能模块拆解用户能干什么系统要撑住什么2.1 前台用户端围绕发现和分享美食设计功能路径美食分享平台的核心是内容。我先把用户的典型行为路径画出来——内容生产、内容消费、互动反馈。这三条路径撑起来了整个站点的功能骨架。内容生产路径用户注册登录后可以发布菜谱。菜谱不是简单的文字图片我拆成了几个独立字段标题、封面图、食材清单、步骤说明、分类归属、自定义标签。食材清单我用 JSON 格式存储因为每道菜的食材数量和单位都不同关系表反而啰嗦步骤说明则是按顺序排列的文本列表。发布之后系统自动生成一条动态推送到美食广场同时同步到作者的个人主页。内容消费路径首页美食广场按时间流展示最新内容和热点内容分类页按八大菜系加烘焙轻食家常菜等场景分类搜索框支持按菜名、食材、作者三种维度查找。用户点进详情页能看到完整菜谱、作者信息、相关推荐。互动反馈路径收藏、点赞、评论、关注。这些功能单独看都很简单但合在一起就是社区活跃度的发动机。我在详情页放了收藏数、点赞数、评论数、浏览数四个指标用户能直观感受到内容的热度作者也有动力持续更新。2.2 后台管理端内容审核和数据统计一个都不能少作为平台方后台管理是所有内容社区项目的刚需。我做了两个大块内容管理菜谱审核新发布的菜谱先进入待审核状态管理员可以一键通过或驳回、分类管理增删改查分类、标签管理、热门推荐位设置、置顶内容管理。用户管理用户列表、状态管理封号/解封、作者认证标记。后台首页放了核心指标今日新增用户、今日发布菜谱数、总菜谱数、总用户数让运营者一眼看到平台状态。这里要特别提醒一点内容审核不要不做也不要过度做。我见过很多个人项目完全没有审核环节结果被人灌了一堆垃圾内容还毫不知情。美食平台虽然内容风险低但图片资源如果被人当图床用服务器带宽分分钟被打爆。所以我设计了先发后审——用户发布后立刻可见但管理员后台能看到所有内容并按需处理这样既不伤害用户体验又能守住底线。2.3 系统支撑能力文件存储、权限控制、定时任务除了业务功能还有几个横切面能力是必须提前规划的认证授权用户端用 JWT 做无状态认证管理员端走独立校验逻辑两个角色权限分开。图片处理用户上传的美食图片系统要压缩出多尺寸版本缩略图、列表图、原图并且做格式统一。定时任务每日热点榜计算、数据统计报表、失效 token 清理、未登录访客的临时数据回收。搜索索引菜谱的标题、食材、标签需要建立高效检索能力。这些能力虽然不直接在前台页面上展示但它们是平台能不能撑住的底层地基。我在做技术选型的时候盯着这几个横切面能力反复对比最终确定了下面的技术栈。3. 技术选型对比为什么是 MyBatis-Plus 而不是 JPA为什么 Redis 不能省技术选型这一步我花了不少时间最终确定的核心技术栈是Spring Boot 2.7 MyBatis-Plus MySQL 8.0 Redis JWT MinIO。下面逐个说理由这些理由都是我在实际编码过程中真实体会到的。MyBatis-Plus 和 Spring Data JPA 之争。说实话JPA 的自动建表和一二级缓存机制很适合快速开发但在做内容社区这种大量自定义查询、多表关联、动态条件分页的场景下JPA 的复杂查询写起来非常憋屈——不是不能写而是要么拼接 Specification要么写 JPQL调试成本高。MyBatis-Plus 的好处是简单的单表 CRUD 直接继承BaseMapper就用复杂的多表查询可以用 XML 自己写 SQL而且它内置了分页插件对列表页条件筛选这种需求简直是量身定做。美食广场需要支持按分类、按标签、按关键词、按时间范围组合筛选这种 SQL 很容易写但用 JPA 能把自己绕晕。另外MyBatis-Plus 的逻辑删除、自动填充创建时间、更新时间也帮我省掉了非常多的样板代码。MySQL 8.0 的选型。从 5.7 切到 8.0最直接的感受是窗口函数真好用。比如某人发布的所有菜谱中按收藏数排名一条 SQL 就能算出来不需要复杂子查询。8.0 的默认字符集是utf8mb4对 emoji 支持不需要额外配置——美食分享平台上用户评论里出现 emoji 的概率极高这个细节不能忽略。Redis 不是锦上添花而是必需品。我有三个核心使用场景点赞数和浏览数的秒级更新先写 Redis 再异步落库热门菜谱榜单的实时计算用户 Session 级别的临时状态。另外首页的今日热点如果每次请求都查 MySQL高峰期压力很大我用 Redis 缓存了热点列表每 5 分钟刷新一次QPS 从几百降到几十。MinIO 和云存储的选择。很多教程喜欢直接用七牛云/阿里云 OSS但在内网部署或个人服务器场景下MinIO 是完全兼容 S3 API 的开源对象存储装在服务器上就能提供图片上传下载服务。我选择 MinIO 的理由是可控不依赖外部厂商环境隔离、数据私有以后想迁云也很方便因为 API 是标准化的。菜谱图片统一走 MinIO配合 Nginx 做静态资源代理实际体验下来很稳定。做技术选型对比时我整理过一个表格大家可以直接抄技术组件选型结果选型理由Web 框架Spring Boot 2.7生态成熟、自动配置、稳定版ORMMyBatis-Plus复杂查询灵活、内置分页、较少样板代码数据库MySQL 8.0窗口函数、utf8mb4、社区资源多缓存Redis热点榜单、计数、异步落库搜索MySQL LIKE 全文索引中小体量够用上 ES 是后面的事文件存储MinIOS3 协议、私有化部署、易迁移认证JWT无状态、易扩展接口文档Knife4j在线调试方便前后端协作神器构建Maven上手门槛低和 Jenkins/Docker 都好集成有个东西我没上——搜索引擎 Elasticsearch。我知道很多内容平台都用 ES 做搜索但说实话在数据量没有到几十万条的时候MySQL 的LIKE模糊查询配合ngram全文索引性能完全够用而且省掉了一套独立的集群运维成本。我在搜索设计里做了个折中菜名和食材字段建了FULLTEXT索引用MATCH...AGAINST做评分排序数据量过 10 万条再考虑迁 ES。4. 数据库设计从用户表到菜谱详情12 张核心表的设计思路4.1 用户、内容、互动三大域建模数据库设计是我在整个项目里最谨慎的一部分。表结构一旦定了后面改起来痛苦无比。我按用户域、内容域、互动域三个域来建模一共设计了 12 张核心表。用户域user用户主表username、passwordBCrypt 加密、nickname、avatar、bio、roleUSER/ADMIN、status、create_time。user_follow关注关系表user_id、follow_user_id。关注关系用单独的表而不在用户表里存关注数是因为关注数是个频繁更新的聚合值放主表里会锁行单独建表后关注数用 Redis 统计即可。内容域category分类表用于菜谱分类如川菜、粤菜、烘焙、轻食。recipe菜谱主表这是整个平台的核心表。字段包括title、cover_image、user_id作者、category_id、description、ingredientsJSON 数组存食材清单、statusPENDING待审核/PUBLISHED已发布/REJECTED已驳回、view_count、like_count、favorite_count、comment_count、create_time、update_time。recipe_step菜谱步骤表recipe_id 关联菜谱、step_no 步骤序号、content 步骤文本、image_url 步骤图。步骤拆成子表而不是拼成一个长文本好处是前端可以按步骤渲染出清晰的流程卡片后续如果要做只看步骤的打印版也很方便。tag和recipe_tag标签表和菜谱标签关联表多对多关系。互动域recipe_like点赞记录表recipe_id、user_id、create_time。recipe_favorite收藏记录表recipe_id、user_id、create_time。comment评论表recipe_id、user_id、content、parent_id支持楼中楼。recipe_view浏览记录表recipe_id、user_id未登录为 null、view_time。4.2 索引、冗余字段和一些取舍建表的时候有几条是我反复强调的原则读多写少的数据加冗余字段不是坏味道。菜谱表里的like_count、favorite_count、comment_count这些计数虽然可以通过 count 查询实时算出来但列表页一次要展示 20 条菜谱每条都去 count 一下数据库直接被打垮至于吗直接实时 count 必然导致性能灾难所以我在recipe主表里冗余了这几个计数字段。计数更新通过 Redis 异步做保证最终一致。所有关联字段都建索引。这是最基础也最容易漏的——recipe.user_id、recipe.category_id、recipe_step.recipe_id、recipe_tag.recipe_id、recipe_tag.tag_id、comment.recipe_id这些外键字段全部建索引。很多新手只给主键加索引等数据量大了分页变慢才发现问题那时候加索引可能已经要锁表了。JSON 字段要克制。食材清单我用 JSON 存储是因为食材这个对象在业务上没有独立的查询维度不太需要按食材搜菜谱以外的场景。但如果你觉得以后要按某个食材筛选所有菜谱这种功能那还是老老实实拆出ingredient子表否则 SQL 里查 JSON 字段会非常痛苦。我的项目里搜索食材用的是全文索引数据存在 JSON 字段里也能被全文索引覆盖到所以够用。最终的数据模型长这样user ├── recipe (user 发布 recipe) │ ├── recipe_step (recipe 的步骤) │ ├── recipe_tag (recipe 和 tag 关联) │ └── category (recipe 属于分类) ├── recipe_like (用户给菜谱点赞) ├── recipe_favorite (用户收藏菜谱) ├── comment (用户评论菜谱) └── recipe_view (用户浏览菜谱)这套模型跑下来的感受是整体够用、扩展性均衡。将来要加菜谱合集合集里包含多道菜或活动页固定一批菜谱参与活动只需要在合集表和菜谱之间加一张关联表不需要动现有表结构。5. 后端核心实现认证、上传、搜索、点赞四个最难啃的点5.1 JWT 认证和用户权限Spring Security 配置别硬抄Spring Security 是所有 Spring Boot 项目里最容易让人翻车的地方因为网上的教程版本差异极大。我这次统一了思路用 Spring Security JWT 做无状态认证自定义 Filter 完成 token 校验不碰 OAuth2 那套重东西。核心逻辑是这样的用户登录成功后端生成 JWT 返回给前端前端存到 localStorage。前端每次请求在 Header 里带Authorization: Bearer token。后端自定义一个JwtAuthenticationFilter继承OncePerRequestFilter在过滤链里解析 token、校验签名、把用户信息塞进SecurityContext。请求进入 Controller 时通过AuthenticationPrincipal拿到当前用户。这里有个容易踩的坑很多人直接把 Security 配置类从网上抄下来结果发现WebSecurityConfigurerAdapter在 Spring Boot 2.7 里还能用在 3.x 里直接废弃了。虽然我用的 2.7 还能用但建议新项目直接使用SecurityFilterChain的 Bean 配置方式避免后续升级痛苦。我贴一下配置的关键代码大家可以参考这个思路Configuration EnableWebSecurity public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { this.jwtAuthenticationFilter jwtAuthenticationFilter; } Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf(csrf - csrf.disable()) .sessionManagement(session - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/**, /api/recipes/list, /api/recipes/detail/**).permitAll() .requestMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling(ex - ex .authenticationEntryPoint((req, res, e) - { res.setStatus(401); res.setContentType(application/json;charsetUTF-8); res.getWriter().write({\code\:401,\msg\:\未登录或登录已过期\}); }) ); return http.build(); } }注意几个细节/api/auth/**和公开的菜谱列表接口要放行后台管理接口单独用hasRole(ADMIN)保护未登录的拦截要返回 JSON不能跳转 login 页面因为这是前后端分离项目。JWT token 的有效期我设成 7 天用户端每次请求时刷新一次过期时间管理员台 token 有效期设成 2 小时权限边界越严越好。5.2 图片上传MinIO 集成与压缩处理美食分享平台的核心资产就是图片一个精致的菜谱封面或步骤图能直接影响用户的浏览意愿。图片上传这个功能我踩过不少坑总结下来最关键的是三点限制大小、压缩处理、安全校验。首先MinIO 的集成非常简单。引入依赖后配置一个MinioClient的 Bean上传时调用putObject就完成Service public class FileStorageService { private final MinioClient minioClient; public FileStorageService(MinioClient minioClient) { this.minioClient minioClient; } public String upload(MultipartFile file, String bucket, String objectName) { try { // 检查 bucket 是否已创建 boolean exists minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build()); if (!exists) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build()); } // 上传文件 minioClient.putObject(PutObjectArgs.builder() .bucket(bucket) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); // 拼接访问路径 return /api/files/ bucket / objectName; } catch (Exception e) { throw new RuntimeException(文件上传失败, e); } } }但直接存原图有两个问题带宽浪费和加载速度慢。所以我开发时做了三件事限制上传文件类型只接受jpg、jpeg、png、webp前端和后端双重校验。按需求压缩封面图统一压缩到宽度 800px步骤图压缩到宽度 600px头像压缩到 200x200。缩略图单独生成列表页加载缩略图详情页加载原图。这里我推荐一个工具Thumbnailator几行代码就能完成图片缩放和格式转换BufferedImage image ImageIO.read(file.getInputStream()); Thumbnails.of(image) .width(800) .outputFormat(jpg) .outputQuality(0.85) .toOutputStream(outputStream);实测下来一张 5MB 的相机原图压缩后大概 300KB 左右加载速度提升非常明显。还有一个细节图片文件名不能用用户上传的原始文件名因为客户端传的文件名可能包含中文、特殊字符甚至路径注入攻击。我用UUID 时间戳 后缀重新生成对象名从根上杜绝安全问题。5.3 搜索功能MySQL FULLTEXT 标题/食材/标签加权做搜索功能之前我认真考虑过要不要上 Elasticsearch最后决定先在 MySQL 上做。上面说了目前的数据量根本不值得引入一套 ES而且 MySQL 8.0 的全文索引对中文支持已经不错了处理中文需要配置ngram分词插件但比我预想的简单——只要建表时指定全文索引查询时用MATCH...AGAINST即可。具体做法CREATE FULLTEXT INDEX ft_recipe_search ON recipe(title, description, ingredients) WITH PARSER ngram;查询的时候我用加权搜索让结果更符合美食平台的用户预期——标题命中的权重最高食材命中的次之描述的权重最低。实现方式是写一条动态 SQL在 MyBatis-Plus XML 里拼接不同字段的MATCH条件score 相加后排序。除了全文搜索我还额外做了标签预筛选。热搜词里大家搜炸鸡或家常菜近义词和场景词不好用关键词索引直接覆盖我把标签作为一种结构化的筛选条件挂到了搜搜接口上——用户搜索后外层再按标签和时间做二次过滤。这样即使用户输入的关键词比较泛也能通过标签精确锚定。如果大家以后真的要把搜索换成 ES核心索引结构大概这样recipe_index存菜谱 id、标题、食谱描述、食材列表、标签列表从 MySQL 同步过去查询的时候走 ES然后拿recipeId回表查详情。这个演进路径很清晰但现阶段还是老老实实用 MySQL。5.4 点赞、收藏和计数先写 Redis 再异步落库点赞/收藏/浏览数这类高频写操作如果每次都直接写 MySQL数据库的锁竞争和磁盘 IO 会很密集。我采用的模式是Redis 计数 延迟异步落库。具体逻辑用户点收藏前端调POST /api/recipe/{id}/favorite。后端先把userId写入 Redis 的 Set 集合favorite:recipe:{id}。接着把计数递增到 Redis 的 Hash 里然后返回收藏成功给前端。每隔一段时间比如 5 分钟一个定时任务把 Redis 里的增量同步回 MySQL 的favorite_count字段。这套模式好处很明显用户侧响应极快数据库压力平摊。但坏处也同样明显万一 Redis 数据丢了计数会不准确。不过对美食分享平台这种业务来说计数偏差个位数完全无感知这属于典型的可用性和一致性的权衡。我的兜底方案是每晚凌晨跑一次全量统计任务把每道菜的真实点赞收藏数刷回 MySQL修正白天可能产生的偏差。有个并发细节要注意不可重复点赞。用户点赞之前要判断Redis Set里有没有这个 userId没有才执行SADD这道判断保证了一个用户对一个内容最多只能点一次赞。用关系型数据库做同样的事情需要在user_id recipe_id上建唯一索引两条路都可以Redis 的做法更快也不产生随机的数据库索引竞争。6. 前后端对接接口设计规范、跨域和文件访问代理6.1 RESTful 接口约定和统一返回结构前后端分离的项目接口设计直接影响开发效率和联调体验。我一开始就定下规范统一返回对象ResultT无论成功失败响应 JSON 结构都一致。public class ResultT { private int code; // 200 成功400 参数错误401 未登录500 服务异常 private String msg; private T data; }接口路径按资源命名POST /api/auth/register注册POST /api/auth/login登录GET /api/recipes/list?page1size10categoryIdxxtagIdxxkeywordxx菜谱列表GET /api/recipes/detail/{id}菜谱详情POST /api/recipes发菜谱POST /api/recipes/{id}/like点赞POST /api/recipes/{id}/favorite收藏GET /api/users/{id}/profile用户主页GET /api/users/{id}/followers粉丝列表我特别建议在开发阶段就接上Knife4jSwagger 的增强版。它能在浏览器里直接测试所有接口对调试 JWT 这类带认证的接口非常方便——在文档页面填好 token点一下就能调用比用 Postman 手动拼参数效率高不少。6.2 跨域处理与文件访问映射前后端分离部署时跨域问题是首要拦路虎。前端跑在http://localhost:5173Vite 默认端口后端跑在http://localhost:8080前端直接发 Ajax 请求会被浏览器拦截。我的处理方式是后端配置 CORSConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意allowCredentials(true)时allowedOrigins(*)在allowedOriginPatterns(*)是可以配合的。实际生产环境建议把allowedOriginPatterns配成真实前端域名不要用*或字符串匹配减少恶意站点的跨域请求。另外MinIO 里存的文件不能直接暴露给前端否则会出现另一个跨域问题而且 MinIO 的外网访问端口如果直接开放也有安全隐患。我的方案是通过 Nginx 反代/api/files/**路径到 MinIO 端口前端看到的 URL 永远是后端域名location /api/files/ { proxy_pass http://127.0.0.1:9000/; proxy_set_header Host $host; }这样图片的访问路径和 API 域名保持一致后端在生成访问 URL 时也不需要硬编码 MinIO 的内网端口。7. 部署上线与性能调优宝塔面板 Docker以及压测后的三个教训7.1 服务器部署方案搜索热词里有不少宝塔docker部署springboot和springboot 阿里云构建地址可见大家对部署环节的关注度非常高。我的部署方案很简单一台 2C4G 云服务器 宝塔面板 Docker Compose。主要跑四个容器MySQL、Redis、MinIO、后端 jar 包。前端打包成静态文件后丢到 Nginx 的html目录反代后端 API。Docker Compose 文件的核心片段version: 3.8 services: mysql: image: mysql:8.0 container_name: food-mysql environment: MYSQL_ROOT_PASSWORD: yourpassword MYSQL_DATABASE: food_share ports: - 3306:3306 volumes: - ./mysql-data:/var/lib/mysql redis: image: redis:7-alpine container_name: food-redis ports: - 6379:6379 minio: image: minio/minio container_name: food-minio command: server /data --console-address :9001 ports: - 9000:9000 - 9001:9001 environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin123 volumes: - ./minio-data:/data backend: build: . container_name: food-backend depends_on: - mysql - redis - minio ports: - 8080:8080用 Docker 部署的好处是环境隔离、迁移方便。如果想精简也可以不用 Docker直接在宝塔面板里安装 MySQL、Redis、MinIO后端 jar 用系统服务进程守护效果差不多看个人习惯。7.2 压测后做的三个优化项目上线前我用 JMeter 做了基础压测模拟 100 个并发用户持续操作 10 分钟。发现的问题和优化办法如下第一个教训首页热点接口响应时间从 50ms 涨到 1200ms原因是每次请求都在实时计算热度值。问题定位后我做了俩改动热点数据 Redis 缓存5 分钟过期热度值从浏览量收藏量3点赞量2的加权积分中取值计算结果直接存 Redis。优化后接口 P99 稳定在 80ms 左右。第二个教训图片懒加载要配占位图否则首屏图片加载数量过多。菜谱列表页一页 20 道菜每道菜一张封面缩略图如果用户网速一般页面会白屏很久。我把封面图全部换成 WebP 格式加载时先显示后端返回的低清占位图目标图加载完再替换体感改善非常明显。第三个教训MySQL 连接池别用默认配置。默认的connectionTimeout30000、maximumPoolSize10在高并发下不够用。我调成了maximum-pool-size: 30minimum-idle: 5connection-timeout: 5000防止长时间排队。同时在application.yml里开启了 MySQL 的连接验证和空闲回收避免长时间空闲连接被数据库服务端断开后应用还拿着死连接去访问。还有一点网上搜springboot定时任务经常能搜到Scheduled的写法我在项目里确实也用了这个注解做每日榜单刷新。不过要提醒一下Scheduled只适合单实例部署。如果以后项目扩到两台机器同时跑后端定时任务会被触发两次这时候就要引入分布式锁或独立的调度服务了。单体阶段用Scheduled完全合理但要心里有数这是后续架构升级时要处理的点。8. 整个项目做完后我最想分享的几个心得项目从立项到上线稳定运行前前后后折腾了三周多。如果让我重新做一遍有几个决策我会坚持有几个时间会被我省下来。坚持用 MyBatis-Plus 而不是 JPA。不是因为 JPA 不好而是内容型社区项目的查询绝大多数是定制化的列表查询MyBatis-Plus 在这种场景下写 SQL 的直观性和可控性远超 JPA。团队协作也更简单新同事接手时看 XML 里的 SQL 就能立刻理解业务逻辑。坚持把用户认证和权限前置设计。我见过太多项目先做业务后补安全最后业务代码里到处散落着判断当前用户的逻辑。我在第一个迭代就把 JWT 认证、角色校验、接口权限全部搭好后面的所有业务开发都在这个安全框架内进行省掉了大量返工。坚持做接口文档而不是口头约定。Knife4j 在开发期和联调期的价值被严重低估了。前后端并行开发时后端把接口定义好注解写清楚前端根本不用追着问参数直接在文档页面看示例、调试接口。这个习惯保持到项目后期能让协作效率提升一半。从热搜词反复出现的springboot项目结构springboot配置springboot 自定义自动配置来看现在很多初学者还在跟配置搏斗。我建议这类同学不要沉迷于从零手写配置先跑通一个完整的业务闭环再回头琢磨底层原理。我这款美食分享平台做完最大的成长不在于学会某个框架的 API而在于理解了一个内容平台的完整生命周期是怎样的——这个理解比具体的技术栈选择更值钱。