ARTICLE DETAIL

资讯详情

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

前后端分离购物推荐网站:SpringBoot+Vue+MyBatis部署实战

前后端分离购物推荐网站:SpringBoot+Vue+MyBatis部署实战 前后端分离购物推荐网站系统从表结构设计到上线部署的完整复盘购物网站这类项目在毕业设计和简历里早就不新鲜了。但如果你打算用它来体现自己的完整工程能力关键不在于又一个商城CRUD而在于怎么让它具备一个可以被讲清楚、被演示、被扩展的独特卖点。这几个月我重新整理了一套前后端分离的购物推荐网站系统技术栈就是标题里那套——SpringBoot做后端接口Vue做前端页面MyBatis管数据库访问MySQL存数据。推荐功能是它的差异点部署则做成了完整可复现的流程。这篇东西适合三类人正在做毕设、准备简历项目、想从只会CRUD进阶到能独立交付一个全栈系统的开发者。我会把核心表结构、推荐策略、部署链路和踩过的坑都过一遍尽量让你照着能跑起来。如果你已经有了SpringBoot和Vue的基础重点看推荐模块的实现思路和部署细节如果你是刚接触前后端分离的新手那就从前到后完整走一遍这套东西本身就是一个很好的学习路径。1. 项目定位为什么不是又一个购物网站1.1 从需求出发购物推荐场景的切入点购物网站的需求其实非常标准化商品展示、购物车、下单、订单管理。单纯做这一套代码写完了你会发现它和培训班作业没有任何区别。这次我加了推荐这个维度——用户登录后首页不再是死板的商品列表而是根据用户历史行为动态生成的推荐流。这样一来项目的技术深度就上来了需要埋点记录用户行为需要设计推荐策略需要写推荐接口还需要在前端做个性化展示。推荐系统听起来很唬人但实际落地时可以控制复杂度。我没打算做真正的协同过滤算法因为那需要大规模用户数据和训练流程对于单体项目来说性价比太低。我采用的是基于行为的加权召回 热度兜底策略后面会详细展开。这套方案在中小规模数据集上完全够用而且可解释性很强——面试时你能说清楚为什么给这个用户推荐了这些商品这比甩一堆算法名词更有说服力。1.2 用户端、管理端、推荐引擎三层功能边界整个系统的功能边界划分得很清晰分三层用户端注册登录、商品分类浏览、搜索、首页推荐流、商品详情、购物车、下单结算、订单查询。管理端商品上下架、分类管理、订单状态处理、推荐位配置比如运营可以手动置顶某些商品。推荐引擎行为上报接口、离线/在线特征计算、召回策略、排序输出。管理端用的是同一个Vue项目通过路由和权限区分角色。前后端分离的项目里权限控制是一个重要考点这里我用了JWT 路由守卫的双重方案后端在接口层校验Token前端在路由跳转时判断是否有权限进入对应页面。1.3 SpringBoot Vue MyBatis MySQL这套组合为什么会成为主流这套技术栈之所以是主流不是因为流行即合理而是因为每一环都有不可替代的位置SpringBoot解决了Spring配置繁琐的问题内嵌Tomcat打一个jar包就能跑。对于中小型项目和初学者来说它把搭环境的成本降到了极低。Vue响应式数据和组件化机制天然适合电商这类交互密集的页面。而且Vue的生态完善Element UI这类组件库拿过来就能写后台管理界面。MyBatisSQL可控性强复杂查询可以直接手写不用受ORM框架的生成规则束缚。特别是做推荐系统时统计用户行为、计算商品热度这类SQL手写比JPA的自动生成清晰得多。MySQL免费、稳定、普及率高。单机百万级数据以下性能完全撑得住。2. 数据库与后端结构设计先定表再写接口2.1 核心表设计从用户到订单再到行为日志的完整链路数据库是整个系统的基础表结构设计好后端代码就顺了。我设计了八张核心表user用户表id、username、passwordBCrypt加密存储、avatar、roleUSER/ADMIN、create_time。category分类表id、name、parent_id支持两级分类、sort_order。product商品表id、category_id、name、subtitle、main_image、price、stock、status1上架/0下架、sales销量、create_time。behavior_log用户行为日志表id、user_id、product_id、behavior_type1浏览/2加购/3下单、score、create_time。cart_item购物车表id、user_id、product_id、quantity、checked。orders订单表id、order_no、user_id、total_price、status0待支付/1已支付/2已发货/3已完成/4已取消、create_time。order_item订单明细表id、order_id、product_id、product_name、product_price、quantity。recommend_config推荐配置表id、product_id、weight、is_active、create_time。这张表用于运营手动调整推荐权重避免推荐结果完全不可控。关于behavior_log它是推荐模块的地基。很多人做推荐系统容易忽略的就是行为数据从哪来其实各种业务动作发生时顺手插入一条行为日志就完事了。我在这里做了一个加分设计score字段不写死浏览记1分加购记3分下单记5分。这样推荐排序时就可以直接按加权求和算用户对某个分类的偏好SQL一条语句搞定。2.2 SpringBoot分层结构与项目目录的规范性后端我没有搞花里胡哨的微服务架构就是最经典的单体分层controller接口层只做参数校验和结果包装。service业务层处理核心逻辑事务注解加在这里。mapper数据访问层只写MyBatis的接口定义。entity数据库实体映射。dto前端交互的数据传输对象。config各类配置类比如跨域配置、MyBatis配置、拦截器配置。common统一返回结果、异常处理、工具类。这层规范简直是项目的第一条生命线。我见过很多同学把业务逻辑写在Controller里一个接口大几百行后面想重构都无从下手。分层的核心价值在于职责单一Controller只负责接收请求、返回响应Service只负责业务规则Mapper只负责数据读写。这套规范坚持下来即使项目规模膨胀一倍维护成本也不会失控。统一返回结果的设计也值得提一下。我定义了一个Result类包含code、message、data三个字段所有接口都返回这个结构。前端axios在拦截器里统一判断code省去了每个页面单独处理错误逻辑的麻烦。实际开发中这是我强烈建议的规范。2.3 MyBatis手写SQL的关键技巧动态SQL、分页查询、批量插入MyBatis最核心的价值就是动态SQL。我在商品列表接口里用到了大量这种场景比如根据分类、价格区间、商品名模糊搜索还要处理不同的排序方式select idselectProductList resultTypecom.demo.entity.Product SELECT * FROM product where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if if testminPrice ! null AND price gt; #{minPrice} /if /where ORDER BY choose when testsort salessales DESC/when when testsort priceprice ASC/when otherwisecreate_time DESC/otherwise /choose /select这样的好处是一个Mapper方法能覆盖多条件组合的查询不用为每一种查询组合单独写接口。分页我用的是PageHelper插件很方便引入依赖后在Service层调用PageHelper.startPage(pageNum, pageSize)紧接着的查询就会被自动加上LIMIT。这里有个新手容易踩的坑PageHelper只对紧接着的第一条查询生效如果你在startPage和Mapper查询之间插入了其他数据库操作分页就会失效。批量插入在订单生成场景特别重要。一个订单的明细可能是多条如果用for循环逐条insert性能会非常差。正确的做法是用MyBatis的foreach标签拼成一条批量插入语句。我自己测过同样插入100条数据批量插入比循环单插快了一个数量级。生成订单号时我用的方案是时间戳 随机数 用户ID后缀保证了单机下不会重复不需要引入分布式ID组件。3. 推荐模块的落地从简单可用到有说服力3.1 推荐策略的选型对比为什么放弃协同过滤做推荐模块前我认真对比过三条路线。第一是协同过滤分基于用户和基于物品两种。它的优势是推荐结果足够个性化但问题也很明显需要用户-物品评分矩阵数据稀疏时效果很差而且Java造轮子实现全量计算成本不低。对于这个项目用户量就几千矩阵计算出来的结果可能还不如热门商品推荐来得实用。第二是基于内容的推荐核心思路是提取物品特征分类、标签、价格区间计算用户偏好和物品特征的匹配度。这个方案的好处是逻辑简单、可解释性强用户点开猜你喜欢时能看到推荐理由。第三是混合推荐用基于内容的方法做主召回再用热度权重做兜底如果有运营手动设置的置顶权重也一并参与排序。最终我选的是第三种的简化版。核心思路是根据用户在过去一段时间内的行为日志统计用户对每个分类的偏好得分然后从用户偏好的分类里召回商品再按商品热度销量行为分数加权排序最终输出推荐列表。这个方案的成本很低效果却很有说服力。用户新注册或者行为数据不足时会退化为热门榜推荐保证页面永远有内容填充。3.2 用户行为埋点与偏好计算的完整实现行为埋点这里我没有在前端做复杂的埋点SDK而是直接在后端业务接口里顺手记录。Service public class BehaviorServiceImpl implements BehaviorService { Override public void recordBehavior(Long userId, Long productId, Integer behaviorType) { BehaviorLog log new BehaviorLog(); log.setUserId(userId); log.setProductId(productId); log.setBehaviorType(behaviorType); // 浏览1分加购3分下单5分 int[] scoreMap {0, 1, 3, 5}; log.setScore(scoreMap[behaviorType]); behaviorMapper.insert(log); } }这样用户查看商品详情时会记录浏览加入购物车时记录加购提交订单时记录下单。不需要前端额外发请求业务代码顺带就做了数据准确性也更高——毕竟前端的任何打点请求都可能被用户跳过或拦截。偏好计算我用了一条聚合SQL。假设要为用户ID1001推荐商品先算出他最近30天对每个分类的偏好权重SELECT p.category_id, SUM(b.score) AS total_score FROM behavior_log b JOIN product p ON b.product_id p.id WHERE b.user_id #{userId} AND b.create_time gt; DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY p.category_id ORDER BY total_score DESC LIMIT 3拿到TOP 3的偏好分类后再从这些分类下取商品按加权热度排序取前20个返回。加权热度的计算方式是销量占比40% 近7天行为数占比60%再加运营配置的weight作为可调节项。3.3 冷启动处理与推荐接口设计冷启动是所有推荐场景回避不了的问题。新用户没有行为数据方案是直接走热门推荐逻辑按销量和行为热度取TOP N返回。新商品没有销量方案是加权公式里保留创建时间衰减因子——近7天上架的商品在热度计算中额外加一个时间加分避免新商品永远不会被推荐。我当时测试的时候发现如果不加这个因子新上架的商品在一个月内都不可能进入推荐流这显然是不合理的。推荐接口的设计也要动脑筋。我只暴露了两个接口GET /api/recommend/home返回首页推荐流规格是10个商品一组支持下拉加载更多。GET /api/recommend/detail/{productId}返回看了又看推荐根据当前商品的分类找同类热门商品。detail接口的SQL更简单就是按分类查TOP 8但限定要排除当前商品本身。很多初学推荐系统的人会忽略接口设计层面的推荐理由展示我特意在接口返回的每个商品上带上recommendReason字段比如根据您的浏览偏好或热门推荐。这个字段在前端展示出来一个推荐系统就活了。4. Vue前端与前后端联调页面是门面联调是核心4.1 Vue项目结构与核心依赖选型前端我用的是Vue 3 Vite Element Plus的组合。Vite比Webpack的启动速度快了不是一点半点开发体验好很多。项目目录我遵循了大部分Vue工程的标准约定views页面级组件比如Home.vue、ProductList.vue、Cart.vue、AdminProduct.vue。components可复用的业务组件比如ProductCard.vue、RecommendShelf.vue。router路由配置定义页面路径和权限规则。store全局状态管理主要保存用户登录信息和购物车状态。api接口请求的统一封装按模块拆成product.js、cart.js、order.js、recommend.js。utils工具函数比如axios实例封装、Token存取、日期格式化。4.2 Axios封装与跨域问题的典型处理Axios的封装是每一次联调稳定的基石。我的request.js做了三件事统一加上请求头Token、统一处理HTTP错误码、统一处理业务错误码并弹出提示。import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) request.interceptors.response.use( response { const res response.data if (res.code 200) { return res } if (res.code 401) { router.push(/login) } ElMessage.error(res.message) return Promise.reject(new Error(res.message)) }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } )跨域问题的解决方案有两种思路。开发环境用Vite的proxy把/api代理到后端端口生产环境用Nginx统一反向代理前后端同源部署。我在开发阶段使用的是Vite代理配置配好后前端写接口时完全不需要考虑域名和端口差异对齐的是相对路径。4.3 推荐流、购物车、订单页面的数据流与状态管理首页推荐流的实现核心就是滚动自动加载。我在组件里监听滚动事件当滚动到底部一定距离时触发下一页加载。配合的store动作是维护一个recommendList数组和pageNum加载成功后concat新数据。这里要注意的是在请求发起前加一个isLoading标记防止滚动过程中重复触发加载。购物车页面我用Pinia管理全局状态。登录后拉取一次购物车列表存到store里无论在导航栏还是购物车页面角标数量都能实时同步。每次添加商品的接口调用成功后直接更新store里的数据不需要重新请求接口体感会流畅很多。前后端联调阶段最实用的调试工具是Vue DevTools。我几乎每次排查问题都靠它看store里的状态和数据流。比如发现首页推荐上拉加载出问题直接看store里的recommendList有没有正常追加马上就能区分是前端逻辑挂了还是后端接口没返回数据。另外浏览器自带的Network面板也必不可少查看接口的请求参数和响应内容比后端打日志更直观。5. 从开发到上线前后端分离项目的完整部署教程5.1 环境准备JDK、Maven、Node、MySQL的版本搭配策略部署之前一定要统一环境版本。我在这上面吃过亏——本地JDK 17服务器JDK 8SpringBoot版本一高一个接口都起不来。最终我用的版本搭配是这样的组件版本说明JDK8稳定且兼容性最好Maven3.8构建和依赖管理Node.js16前端构建环境MySQL5.7 或 8.0生产建议8.0SpringBoot2.7.x避免使用3.x配置方式差异较大Vue3.4.x配合Vite 5MySQL 8.0和5.7有个很隐蔽的差异就是默认的认证插件不同。如果使用8.0最好在连接串里显式加上时区配置和SSL关闭配置否则会像我第一次部署时那样反复报SSL连接错误或者时区错误。数据库初始化直接执行我写好schema.sql和data.sql即可生产环境里我额外加了一步用mysqldump做定期备份的定时任务避免数据丢失。5.2 后端打包与启动从jar包到进程守护后端打包非常简单在项目根目录执行mvn clean package -DskipTests执行完后target目录下会生成一个可执行的jar包。我习惯用Maven的spring-boot-maven-plugin打包打出来的jar包含了内置Tomcat直接就能运行。启动时注意几个细节先建好独立目录比如/opt/shop把jar文件放进去。数据库初始化要提前完成确保数据库连接配置正确。首次启动用nohup java -jar shop-backend-0.0.1-SNAPSHOT.jar --server.port8080 app.log 21 方式后台运行日志写到app.log里方便排查问题。生产环境我一般还会检查配置里的spring.datasource.url是否把serverTimezoneAsia/Shanghai和useSSLfalse加上了。如果你的服务器内存只有2G建议给JVM做下限制java -Xms256m -Xmx512m -jar这样跑内存不够时会明显影响接口响应速度尤其在推荐模块需要查多张表的时候。5.3 前端构建与Nginx反向代理配置前端打包命令npm install npm run build完成后生成dist目录把这个目录上传到服务器指定位置然后重点来了——Nginx的配置决定了前后端能不能和谐共处。server { listen 80; server_name your-domain.com; root /opt/shop/dist; index index.html; # 前端路由history模式必须配置 location / { try_files $uri $uri/ /index.html; } # API反向代理到SpringBoot location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files这一行是专门给Vue Router的history模式用的。前端路由切换是纯前端行为但用户手动刷新页面时Nginx会按路径去找物理文件找不到就返回404。有了try_files所有找不到的路径都会回退到index.html由Vue Router自己解析路由刷新问题就解决了。location /api/的反向代理是另一个关键点。我这里用了proxy_pass http://127.0.0.1:8080/;注意末尾的斜杠很重要它会把请求路径里的/api替换为/再转发给后端。所以前端请求的是/api/product/list后端收到的就是/product/list配合后端接口定义的/product/list路径不需要额外处理路径前缀。5.4 部署过程中的常见问题与排查思路部署中的坑我把它集中到这个表里现象可能原因解决办法后端启动报错Cant connect to local MySQL serverMySQL没启动或端口不对检查systemctl status mysqld确认3306端口监听数据库连接报SSL错误MySQL 8.0要求显式关闭SSL或配置证书连接串加useSSLfalse前端刷新404Nginx缺少try_files回退配置按上面的Nginx配置补上接口404或404前后端路由/路径对不上查看后端日志确认实际接收路径调整proxy_pass中文乱码数据库字符集不是utf8mb4MySQL配置文件加character-set-serverutf8mb4并重建库表页面能打开但接口全部失败前端构建时baseURL配置了绝对地址dev环境用相对路径/api配合Nginx代理排查这类问题我最常用的方法是看日志后端tail -f app.log实时盯输出。前端则直接在浏览器开发者工具的Network里看接口报什么错基本能定位80%的问题。6. 这套项目踩过的坑与给后来者的建议6.1 数据库连接问题的一次完整排查链路第一次部署时后端启动直接报了错提示信息是ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock排查思路其实很固定。先确认MySQL在跑systemctl status mysqld发现服务确实没有启动。启动MySQL后再看报错变了成了之前提过的SSL连接异常。于是我在数据库连接串里加了useSSLfalseserverTimezoneAsia/Shanghai重启后终于正常。这个经历让我意识到部署问题90%出在环境配置上跟业务代码没任何关系。遇到报错别急着去翻代码先确认环境配置是否正确。6.2 MyBatis中容易忽略的三个细节MyBatis用久了有几个细节会回踩。第一个是XML映射文件的位置。SpringBoot默认扫描的是classpath*:mapper/*.xml如果XML文件放错了位置运行时会报Invalid bound statement这个经典错误。但是这种报错只在运行时才会出现编译期完全察觉不到。所以务必检查application.yml里有没有正确配置mybatis.mapper-locations。第二个是resultType和resultMap的选择。简单查询用resultType最省事但遇到多表关联查询返回结果包含多个实体时就得用resultMap做映射否则字段对不上查出来的数据全是null。这个坑排查起来很隐蔽需要逐字段对比才能发现。第三个是MyBatis的二级缓存。它是基于namespace的也就是一个Mapper共享一份缓存如果Mapper的SQL语句参数很大缓存命中率低不说还有可能造成脏读。我的建议是拿不准就关闭全局二级缓存一级缓存默认对单次会话查询基本够用。面试时能把缓存机制讲清楚也容易加分。6.3 前端联调时的跨域、鉴权与状态同步跨域问题在开发环境可以通过Vite proxy解决我没有写任何后端的CORS配置。但如果你要临时对外开放某个接口的调试权限可以在后端临时加一下CORS配置生产环境建议还是以Nginx代理为主前后端保持同源最省心。鉴权这里我把JWT拦截器做好了之后才体会到请求拦截这个设计的意义除了登录接口外所有/api/order、/api/user、/api/cart开头的接口都需要校验Token。后端拦截器校验失败时返回401前端axios响应拦截器捕获401后跳转登录页。前后端各守一道防线权限就不会出漏洞。还有一个值得注意的小细节就是购物车状态的同步。用户登录后首页导航栏的购物车角标应该显示购物车商品总数。如果这个问题没处理用户添加了商品但角标不更新会误以为是失败。我的处理方式是登录成功后立刻拉一次购物车数据存到store每次加购成功后更新store里的数量不用重新请求接口。状态流清晰了产品体验就扎实了。6.4 这个项目面试时怎么讲才能出彩项目做完了只是第一步能讲清楚比做出来更重要。面试时我建议按这个逻辑组织话术首先说项目背景——它不只是一个标准商城还有一个基于用户行为数据的推荐模块。然后讲数据链路——用户行为在前端哪些节点产生后端如何记录推荐策略如何利用这些数据最终如何输出到前端页面。接着讲技术亮点——MyBatis复杂SQL的编写能力、JWT权限控制方案、前端状态管理如何解决跨页面同步问题。最后讲一次印象深刻的排错经历——部署时的MySQL连接问题或者MyBatis绑定的报错重点讲排查思路。推荐模块是最容易出彩的千万把它当重点讲透。面试官一听数据结构、推荐策略、冷启动处理、运营兜底这些词就知道你是真的做过这套系统而不是把别人的Demo抄了一遍。结尾的小扩展思路这套系统跑通之后扩展方向其实不少。比如给推荐模块增加用户标签功能从行为日志里提取用户的常购分类偏好存到user_profile表或者把首页的推荐流改成由运营在管理端配置的多个榜单又或者引入Redis做首页推荐流的缓存减轻数据库压力。如果你还在纠结毕设选题我会建议拿基于用户行为的个性化商城推荐系统这个方向去做延伸前后端分离落地这些细节都能用得上。根据我的实际经验一个项目只要做到完整可复现、细节能讲清楚、疑难可排查它对你的价值就已经超过大多数纸上谈兵的项目了。
返回列表