资讯详情

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

📅 2026/9/30 3:09:06 | 华诺云谱 👁 阅读
基于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、passwordBCrypt 加密、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 里的防重 keykey 是user_id 接口名 目标 id设置了 2 秒过期第二次请求直接返回操作频繁。5. 联调排错实录前后端分离项目里最容易踩的几个坑5.1 跨域配置冲突CorsFilter 与 CrossOrigin 同时配置导致的诡异问题前后端分离开发时第一个绕不开的问题就是跨域。我一开始在 Spring Boot 里写了一个CorsFilter配置类又在部分 Controller 方法上加了CrossOrigin注解。结果明明配置看起来没问题前端请求却时不时报跨域错误。排查了很久才发现当CorsFilter已经对请求进行了跨域处理并且响应头里已经带上了Access-Control-Allow-Origin时如果某个方法上还有CrossOriginSpring 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里我配置了三个 profiledev、test、prod。开发时用dev连接本地 MySQL 和 Redis发布时用prod连接云服务器上的数据库。打包命令是mvn clean package -DskipTests -Pprod打包出来的 jar 通常在target目录下执行java -jar perfume-platform.jar就能启动。为了观察日志我加了--spring.profiles.activeprod参数指定环境再用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 记录本身就是一份很漂亮的项目文档写毕业设计说明书或者项目总结的时候帮助非常大。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑