拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Spring Boot的香水分享平台开发实战

基于Spring Boot的香水分享平台开发实战

逛香水论坛的时候经常能看到各种"求推荐""求拔草"的帖子,但信息散落在评论区里,找起来很费劲。有些朋友想认真记录自己用过的每一瓶香水,却找不到一个顺手的工具。后来我在做毕业设计和项目练手时,刚好想找一个既有业务深度又不至于太复杂的题目,就决定做一个基于 Spring Boot 的香水分享平台。这个项目做下来,从数据库建模到权限控制,从图片上传到前后端分离联调,基本把 JavaWeb 开发的整套流程都过了一遍,特别适合正在准备毕设、或者想从增删改查进阶到独立搭系统的同学参考。

这个平台的核心定位是"社区 + 内容管理":用户可以注册登录、浏览香水库、发布香水信息、写评价打分、收藏喜欢的香水,还可以关注其他香友。整套系统采用 Spring Boot + Vue 前后端分离架构,后端提供 RESTful API,前端用 Vue 配合 Element UI 搭建管理页面和展示页面。下面我会按照项目的实际推进顺序,把技术选型、表结构设计、核心功能实现、踩坑排错这条线完整地讲一遍。

1. 需求分析与功能边界:先想清楚这个平台到底做什么

1.1 从场景倒推功能清单

我在动手写代码之前,先列了几个核心使用场景:

  • 一个香水小白想按照"花果调""木质调""清新调"等分类去逛香水,了解主流香型的代表产品。
  • 一个资深香友想要发布自己新入手的香水,并配上照片和香调描述。
  • 一个用户用了一段时间某款香水,想把真实感受分享出来,给一个 1 到 5 星的评分。
  • 用户遇到自己喜欢的香水或香评,希望收藏下来以后再看,也想关注那些品味相近的香友。

从这些场景倒推,功能模块就很清晰了:用户模块、香水分类模块、香水信息管理模块、评价模块、收藏模块、关注模块,外加一个轮播图和公告的信息展示模块。管理员端还需要具备香水审核、用户管理等能力,不过实际上我在做这个项目时,把管理端功能压缩到了后端接口层,前端管理页面只做了最核心的几个页面,优先保证核心链路跑通。

1.2 合理划定 MVP 边界

这里想给一个过来人的建议:毕业设计或项目练手最忌讳的就是功能贪多。我第一版的需求清单里有过"香水对比""社区话题""私信聊天"这些东西,后来全部砍掉了。砍掉的理由很简单——这些功能对数据库设计和代码复杂度的要求会成倍上升,但展示出来的核心价值并不高。

最后确定的 MVP 边界是:

  • 游客只能浏览香水库和热门评价,不能进行任何写操作。
  • 注册用户可以发布香水、写评价、收藏、关注,只能操作自己的数据。
  • 管理员负责分类管理、香水的上下架、用户禁用等后台操作。

权限分层越简单,代码里就越不容易出现漏洞。实际开发中,我用了拦截器做登录校验,配合一个简单的@RequireLogin自定义注解来判断接口是否要求登录,管理员接口额外校验角色。这个方案比引入 Spring Security 全家桶要轻得多,对理解权限控制的底层原理也更有帮助。

2. 技术选型逻辑:Spring Boot + Vue 前后端分离为什么是首选组合

2.1 后端框架:Spring Boot 的版本选择与理由

Spring Boot 选的是 2.7.x 系列,而不是最新的 3.x。原因很实际:2.7 版本是 javax 包名,网上绝大多数的教程、开源项目、毕业设计参考代码都基于这个版本,遇到问题更容易搜到解决方案。Spring Boot 3 切换到了 jakarta 包名,很多老项目依赖直接不兼容,光是把第三方 SDK 从 javax 适配到 jakarta 就要花掉不少时间。除非你从一开始就确定要用 GraalVM 原生镜像之类的特性,否则做业务系统老老实实用 2.7 最稳。

Spring Boot 在这个项目里承担的事情包括:

  • 内置 Tomcat 容器,打包成 jar 之后一条命令就能启动,部署成本几乎为零。
  • 自动配置机制帮我把数据源、MyBatis、Redis、文件上传这些基础设施的配置简化到了极致。
  • 配合 Spring MVC 提供 RESTful 接口,前后端通过 JSON 通信,双方可以完全并行开发。

2.2 数据访问层:MyBatis-Plus 的取舍

数据库访问层我用了 MyBatis-Plus。有人说 MyBatis-Plus 太"无脑",写不出复杂 SQL,但我个人觉得在业务系统里,80% 的操作就是单表 CRUD 和简单的条件分页,MyBatis-Plus 的BaseMapper和LambdaQueryWrapper能省下大量重复的 XML 编写工作。真正复杂的多表关联查询,我采用手写 XML 的方式补充,互不冲突。

举个例子,香水列表页需要显示发布者的昵称,还要统计每个香水的评价数量,这个查询跨了perfume_info、user、review三张表,MyBatis-Plus 的单表封装就搞不定了,我就在 XML 里写了一段自定义 SQL。模块化组合使用,比一条道走黑灵活得多。

2.3 缓存与文件存储:Redis 和 MinIO 的配合

评价列表的点赞数、香水详情的浏览量这些高频读取数据,我放到了 Redis 里,每次点赞先操作 Redis,再由定时任务异步刷入 MySQL。这个设计在并发量上来之后会明显减轻数据库压力,而且代码逻辑并不复杂。文件存储用了 MinIO,本地开发时也可以退化为存储在服务器磁盘路径,通过 Spring Boot 的静态资源映射对外提供访问 URL。

技术栈最终确定为:

层级选型说明
后端框架Spring Boot 2.7.x稳定、生态成熟、资料多
持久层MyBatis-Plus + MySQL 8.0单表快速 CRUD,复杂 SQL 手写
缓存Redis 5.x点赞数、浏览量、验证码缓存
文件服务MinIO/本地磁盘香水图片、评价图片存储
前端Vue 2 + Element UI + Axios管理后台和展示页面共用
构建Maven标准多模块或单模块结构

3. 数据库建模:香水领域的实体关系与表结构设计

3.1 核心表设计背后的思考

数据库表是系统的地基。我设计表的时候先画了一张简单的 ER 关系图,然后用 PowerDesigner 生成建表 SQL,再手工调整字段注释和索引。核心表有这几张:

  • user用户表:id、username、password(BCrypt 加密)、nickname、avatar、gender、bio、status、create_time。
  • category香水分类表:id、name、parent_id、sort。支持二级分类,比如"花香调"下再细分"白花调"和"粉花调"。
  • perfume_info香水信息表:id、name、brand、category_id、description、top_note(前调)、middle_note(中调)、base_note(后调)、concentration(香精浓度)、gender_orientation(适合性别)、price、image_url、status、created_by、create_time。
  • review评价表:id、user_id、perfume_id、rating、content、images、like_count、status、create_time。
  • favorite收藏表:id、user_id、perfume_id、create_time,加唯一索引(user_id, perfume_id)。
  • follow关注表:id、user_id、target_user_id、create_time,同样加唯一索引。
  • banner轮播图表:id、image_url、link_url、sort、status。

有几处设计细节是后来通过实际踩坑才完善的,值得展开说说。

3.2 评价表的冗余字段:值得还是不值得

review表里我冗余了perfume_id和user_id,这是多对多关系的中间表逻辑,必然要带上。后来我加了一个rating字段专门存储评分,而没有直接在香水表里维护平均分。原因是每次查香水列表时如果动态AVG(rating),在数据量大的时候会拖慢列表查询;但如果在香水表里冗余一个avg_rating字段,就必须在每次有新评价或者评价变更时同步更新。

我最终采用的做法是:在perfume_info表里保留avg_rating和review_count两个冗余字段,评价表review每次 insert、update、delete 时,通过一个事务内的统计 SQL 重新计算这两个冗余字段。香水的列表页、详情页展示平均分时就不需要再做聚合运算了,性能好很多。

3.3 表索引设计的教训

最初建表时我只给主键 id 建了索引,结果在开发环境数据量很小的时候完全没有感觉,等导入了大约 2 万条香水测试数据后,按分类翻页明显变慢。后来在perfume_info表的category_id、status和create_time上加了复合索引,在review表的perfume_id上加普通索引,分页查询的速度提升了将近一个数量级。创建索引的 SQL 类似:

ALTER TABLE perfume_info ADD INDEX idx_category_status_time (category_id, status, create_time); ALTER TABLE review ADD INDEX idx_perfume_user (perfume_id, user_id);

这里有个容易被忽略的点:复合索引的字段顺序很重要。我把category_id放在最前面,是因为查询条件里最常用的是"某个分类下、状态正常、按时间倒序"这个组合。如果顺序调换,索引就不一定能被高效使用。

4. 核心功能落地:从接口设计到业务实现的完整链路

4.1 用户注册登录与验证码

用户模块的接口设计遵循 RESTful 风格,不在 URL 里出现动词。注册接口POST /api/user/register接收用户名、密码、确认密码三个字段,后端做参数校验后,密码使用 BCrypt 加密存入数据库。BCrypt 的好处是每次加密的盐值随机,即使两个用户密码相同,密文也不同,安全性比简单的 MD5 高得多。

登录接口POST /api/user/login校验通过后,我用 UUID 生成一个 token,以login:token为 key 存入 Redis,设置 30 分钟过期。前端把 token 存在 localStorage,每次请求在Authorization头里带上。拦截器里从 Redis 查 token,查不到就返回 401。这个方案比 JWT 更简单的地方在于:管理员要禁用某个用户时,直接把 Redis 里的 token 删掉,立刻生效,不需要等待过期时间。

短信/邮箱验证码我做了简化处理,直接用图形验证码。验证码生成采用 Java 的BufferedImage手绘,存到 Redis 时设置 5 分钟过期,校验通过后立即删除。开发中容易忽略的一点是:每次生成验证码之前要先删除旧的 key,否则用户在验证码过期前反复点击刷新,Redis 里会堆积多个无效 key。

4.2 香水发布与图片上传

香水发布的表单字段比较多:名称、品牌、分类、香调三列(前调/中调/后调)、浓度、价格、描述、图片。前端通过 Element UI 的el-upload组件先把图片传到POST /api/upload接口,拿到返回的图片 URL 后,再把整个表单提交给POST /api/perfume接口。

图片上传接口我在实现时做了一个限制:单张图片大小不超过 5MB,支持 jpg、png、webp 三种格式。存储路径按照yyyy/MM/dd组织目录,文件名用UUID + 原扩展名,避免重名覆盖。上传到 MinIO 时需要特别注意 bucket 的访问权限设置——如果 bucket 是私有读权限,那么前端直接通过 URL 访问图片会得到 403,必须在代码里生成临时预签名 URL;如果 bucket 是公共读权限,那在 Nginx 也设置允许跨域访问即可。我这个项目里偷懒用了公共 readonly 权限,但生产环境更推荐私有权限加预签名 URL 的方案,防止图片被恶意遍历下载。

4.3 评价、收藏、关注模块

评价接口POST /api/review收到的 JSON 里包含perfumeId、rating、content、images(图片 URL 数组)。这里有三个必须注意的点:

  • 一个用户对同一瓶香水只能发一条评价。我在review表上加了一个联合唯一索引(user_id, perfume_id),并在插入前先用select count做一次前置校验。索引兜底保证并发情况下也不会插入重复数据。
  • 评分范围是 1 到 5 的整数,前端滑块传 0.5 的倍数,后端统一转为整数存储,避免浮点精度问题。
  • 评价发布成功后,需要更新perfume_info里的avg_rating和review_count,这步操作要放在同一个事务里,否则会出现评价写成功但统计数字没更新的情况。

收藏功能的实现很经典:先查favorite表是否存在记录,存在就删除(取消收藏),不存在就插入(收藏),类似开关切换。关注功能的结构和收藏几乎一样,只是表名和目标不同。两个接口我都做了防重复提交的处理——前端在按钮点击后立刻置为 loading 状态,后端在进入业务代码前查一次 Redis 里的防重 key(key 是user_id + 接口名 + 目标 id,设置了 2 秒过期,第二次请求直接返回"操作频繁")。

5. 联调排错实录:前后端分离项目里最容易踩的几个坑

5.1 跨域配置冲突:CorsFilter 与 @CrossOrigin 同时配置导致的诡异问题

前后端分离开发时第一个绕不开的问题就是跨域。我一开始在 Spring Boot 里写了一个CorsFilter配置类,又在部分 Controller 方法上加了@CrossOrigin注解。结果明明配置看起来没问题,前端请求却时不时报跨域错误。

排查了很久才发现:当CorsFilter已经对请求进行了跨域处理,并且响应头里已经带上了Access-Control-Allow-Origin时,如果某个方法上还有@CrossOrigin,Spring MVC 在处理方法返回值的时候会根据注解再执行一次跨域判断,在特定条件下产生重复的响应头,浏览器就会拒绝这个响应。

解决方案很简单:要么全局限在CorsFilter里配置,要么在 Controller 上用注解,二选一,不要混用。我最后保留了CorsFilter方案,把 Controller 上的@CrossOrigin全部删干净。

5.2 图片上传后访问 404,重启项目图片丢失

开发阶段我把图片存到了本项目的static/upload目录下。第一次上传成功后,页面能正常显示图片,但重启项目后图片全部 404。原因是 Spring Boot 打包成 jar 之后,static目录是只读的,而且每次重新构建会清空旧文件。

后来的做法是:在配置文件里指定一个外部存储路径,比如app.upload-path=/data/perfume-upload/,然后通过配置类把这个绝对路径注册为一个静态资源映射:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadPath); } }

这样图片存在服务器磁盘的独立目录里,项目重新部署也不会丢失。如果后续迁移到 MinIO,只需要把上传逻辑替换掉,访问路径保持/upload/**不变,前端代码甚至不用改。

5.3 越权操作的漏洞:只校验了"是否登录"没校验"是否是本人"

这是我后来 review 代码时发现的严重漏洞。最开始我设计的PUT /api/review/{id}接口只做了登录校验,随便登录一个账号就能改掉别人的评价。修复方式是在业务代码里增加一个归属校验:

Review review = reviewMapper.selectById(id); if (review == null || !review.getUserId().equals(currentUserId)) { throw new BizException("无权操作该评价"); }

类似的逻辑还用到了香水信息的修改、删除接口上。这里想特别提醒:越权漏洞在功能测试时很难发现,因为正常用户不会主动改别人的数据,但只要有安全意识的人尝试一下就能攻破。这个问题的根子在于后端默认信任了前端的传参,正确的做法是所有涉及资源归属的接口,都以后端登录态中的 userId 为准,而不是以请求体里的 userId 为准。

5.4 点赞数在并发下的丢失:Redis 补偿方案

一开始点赞数直接用UPDATE review SET like_count = like_count + 1 WHERE id = ?,在低并发下没问题。后来我用 JMeter 模拟 200 个并发请求同时点赞,发现最终的 like_count 和实际点击次数对不上,丢失了将近 20 个。

原因和 InnoDB 的行锁机制有关:同一行的 update 语句会排队执行,如果事务里还包含其他查询逻辑,持有锁的时间更长,同时大量请求堆积还会拖慢整体响应。

解决方案调整为:点赞时只更新 Redis 里的计数器,使用 INCR 命令保证原子性,同时把点赞的用户 id 写入 Redis 的 Set 集合。每 5 分钟由定时任务读取 Set 里的用户列表,统一批量更新数据库。这个方案既能保证实时展示点赞总数(从 Redis 读取),又能让数据库的更新压力变得平缓。

6. 部署上线:从 Maven 打包到服务器运行的完整流程

6.1 Maven 打包配置与多环境切换

项目结构我采用的是标准单模块,所有的代码都在一个src/main/java下,按照controller、service、mapper、entity、config、common分包。有人说单模块不利于复用,但对于这类业务系统,单模块的部署和调试是最快的。

在application.yml里我配置了三个 profile:dev、test、prod。开发时用dev,连接本地 MySQL 和 Redis;发布时用prod,连接云服务器上的数据库。打包命令是:

mvn clean package -DskipTests -Pprod

打包出来的 jar 通常在target目录下,执行java -jar perfume-platform.jar就能启动。为了观察日志,我加了--spring.profiles.active=prod参数指定环境,再用nohup放到后台运行。如果是比较正式的部署,建议用 systemd 写一个 service 文件,让进程崩溃后能够自动重启。

6.2 Nginx 反向代理与前端静态资源托管

前端 Vue 项目执行npm run build之后,生成的dist目录包含所有的静态文件。部署时我直接把dist目录放到 Nginx 的html目录下,然后配置反向代理:

location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }

这样前端的 API 请求地址始终是/api/...,由 Nginx 转发给后端 Spring Boot,不需要前端关心后端的真实 IP 或端口。/api/前缀和后端接口路径的对应关系要约定好:我在 Spring Boot 的配置里给所有接口加了context-path: /api,这样POST /api/user/login在 Nginx 转发后才会正确到达后端的/user/login。

6.3 系统上线后的服务器优化项

项目上线跑了一段时间后,我做了几个优化:

  • 开启 Tomcat 的线程池调优,核心线程数和最大线程数的比例结合服务器配置调整,避免默认配置下高峰期的线程频繁创建销毁。
  • MySQL 连接池设置了maximum-pool-size: 20,并增加了连接有效性检测,防止数据库重启后连接池里的旧连接全部失效。
  • 前端开启了 gzip 压缩,图片通过 Nginx 配置了缓存过期时间,整体首屏加载速度提升明显。

7. 从这套代码还可以扩展出的进阶方向

项目本身做完之后,我在思考它还能往哪些方向发展。如果你在这个项目基础上继续做,有几个方向性价比很高:

  • 香水推荐系统:基于已评价的香水香调标签,计算用户之间的余弦相似度,给用户推荐其他香友喜欢且自己没闻过的香水。推荐逻辑可以先用离线任务算好,存储到 Redis,接口直接读取。
  • 社区动态流:把关注的人发布新香水或新评价的事件写入一个 feed 表,用户打开首页就能看到关注者的最新动态,类似简单版微博时间线。
  • 积分与等级体系:用户发香水、写评价、被点赞都可以获得积分,积分对应不同的香友头衔。这个功能能明显提升用户的活跃度和留存。

在我实际开发的过程中,最大的感受是:这个项目的难点从来不是某个单个技术点有多难,而是怎么把 RO 权限控制、事务一致性、缓存与数据库的数据同步、前端与后端的契约约定这些事情串起来,形成一个整体。只要你完整地做一遍,这些经验是看多少教程都换不来的。

最后分享一个小技巧:如果你也在做类似的 Spring Boot 实战项目,建议从第一行代码开始就用 Git 管理版本,每个功能模块完成之后提交一次,在每一个提交信息里写清楚这次改了什么。等到项目结束,你会发现那份 commit 记录本身就是一份很漂亮的项目文档,写毕业设计说明书或者项目总结的时候帮助非常大。

返回列表