ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue办公直售推荐系统毕设开发与联调全记录

SpringBoot+Vue办公直售推荐系统毕设开发与联调全记录 这个项目我前后折腾了大半个月从最开始只有一个模糊的产品设想到最后把整套 SpringBootVue 的日常办公用品直售推荐系统平台完整跑通源码、SQL脚本、接口文档全齐里面踩过的坑完全够单开一篇。今天这篇就把这个项目的选型思路、核心设计、SQL脚本编排、接口文档对接、前后端联调以及运行过程中最容易出的问题都摊开讲一遍。如果你正在做 Java Web 方向的毕业设计或者准备拿一个完整项目去面试、去答辩这篇可以直接对照着参考。先说清楚这套系统到底是干什么的后端用 SpringBoot 提供 RESTful 接口前端用 Vue 做页面交互数据库用 MySQL项目里配了一份完整的 SQL 初始化脚本另外还有一份能直接对接前后端的接口文档。业务上它覆盖了一个直售平台最典型的全链路用户注册登录、商品浏览和分类检索、购物车管理、下单结算、订单查询以及一个基于商品分类和用户行为的推荐模块。日常办公用品这个场景选得挺聪明因为商品种类固定、价格区间稳定、用户的复购行为也相对明显天然适合做推荐系统的落地演示又不会像淘宝京东那样复杂到没法在毕设周期内完成。下面我从项目整体设计讲起逐步拆到后端、数据库、前端和联调最后集中聊常见问题。这篇文章里的操作都是我在真实环境里跑过的你可以直接照着来。1. 项目整体设计与思路拆解1.1 为什么选 SpringBoot Vue 这个组合毕设选型这件事情真的不用太花哨。SpringBoot Vue 已经算是最好的“稳定牌”。很多同学纠结要不要用 SSM或者是不是该上微服务、Spring Cloud我的建议很直接如果你的目标是完整毕业、顺利答辩而不是搞一个实验室级别的分布式架构那就老老实实用 SpringBoot Vue。SpringBoot 解决的是后端开发的繁琐配置问题。以前写 SSM 需要维护一堆 XML 配置文件数据源、事务管理器、MyBatis 的 SqlSessionFactory、Spring 的 bean 扫描路径等等光配置就能劝退一批人。SpringBoot 用 starter 机制把这些东西全部简化成了几个依赖坐标自动配置帮你在启动时完成大部分初始化工作。你只需要关心业务代码这也正好符合毕设的项目周期。Vue 的价值则在交互端。react、angular不是不好但对一个 Java 为主修方向的学生来说Vue 的学习曲线最平滑。组件化开发、vue-router 做路由、vuex 或 pinia 做状态管理一个中小型系统的前端页面很快就能搭出来。做日常办公用品直售这种系统页面的典型复杂度就是登录注册页、商品列表页、商品详情页、购物车页、订单页、后台管理页用 Vue 写起来非常顺手。最关键的还是前后端分离这个架构本身。前端工程和后端工程可以独立开发、独立测试联调时只要约定好接口文档就行。毕设答辩的时候老师问你“前后端是怎么交互的”你完全可以回答前端通过 axios 调用后端 REST APIJSON 作为数据交换格式接口文档作为契约。这个回答基本滴水不漏而且你真这么做说明你真的理解了现代 Web 开发的协作方式。1.2 业务模块怎么划分才合理很多人拿到这个题目就着急写代码结果做着做着发现代码全堆在一起改一个功能就要动一整片。这就是模块划分没做好。日常办公用品直售推荐系统从业务域来看可以拆成四条主线第一条是用户线。注册、登录、个人信息维护。这条线对应的是系统的最基础能力也是推荐系统识别用户的前提。你要知道这个人是谁才能谈得上为他推荐什么。第二条是商品线。商品分类、商品列表、商品详情、商品搜索。日常办公用品有一个特点就是分类明确书写工具、纸品本册、桌面收纳、办公电子、耗材配件。这个分类结构很重要因为它是推荐系统实现“基于内容的推荐”的天然锚点。第三条是交易线。购物车、订单创建、订单列表、订单状态流转。这是直售平台的业务闭环也是答辩时最容易被追问的地方。你设计订单时要考虑到订单状态待支付、已支付、待发货、已完成、已取消。哪怕支付功能是模拟的后端也要留一个状态字段让整个流程是完整的。第四条是推荐线。推荐模块不能是挂在商品列表下拉刷新的一个推荐位那么简单它需要有自己的数据支撑用户浏览记录、用户收藏或加入购物车的商品、商品类的热度统计。基于这些数据生成一个推荐列表接口前端在首页专门开辟一个“为你推荐”区域来承接。我见过不少同学习惯把业务逻辑全堆在 Controller 里一个方法几百行看起来能跑实际上完全没有分层意识。在毕设阶段分层的价值不仅体现在代码可维护性上更体现在答辩时的表达能力上。Controller 只负责接收参数和返回结果Service 层处理业务规则Mapper 层负责数据库交互Entity 对应表结构这是最基础但也最能体现工程素养的分层。1.3 我推荐的项目结构组织方式先给出一套可以直接复用的后端目录结构包名可以换成你自己的:com.office.sale ├── controller // 接口层接收前端请求返回统一结果 ├── service // 业务层处理核心逻辑定义事务边界 │ └── impl ├── mapper // 数据访问层与数据库交互 ├── entity // 与数据库表结构对应的实体类 ├── dto // 数据传输对象用于接口参数校验和返回 ├── config // 配置类跨域、拦截器、分页插件等 ├── common // 公共类统一返回结果、异常处理、常量定义 ├── util // 工具类JWT生成解析、日期处理等 └── SaleApplication.java // 启动类前端部分我通常这么整理src ├── api // 所有接口请求封装在一个目录 │ ├── user.js │ ├── goods.js │ ├── order.js │ └── recommend.js ├── router // 路由配置含登录守卫 ├── store // 全局状态管理保存用户信息 ├── views // 页面文件夹 │ ├── Login.vue │ ├── Register.vue │ ├── Home.vue │ ├── GoodsList.vue │ ├── GoodsDetail.vue │ ├── Cart.vue │ ├── OrderList.vue │ └── Admin ├── components // 公共组件 ├── utils // axios实例、公共方法 ├── App.vue └── main.js这种结构的好处是前端和后端都能一眼看出来“哪块代码管什么”。尤其是答辩前做演示老师随便指一个文件你都能快速说明白这是干什么的。别小看这一点很多同学项目能跑但结构混乱演示起来非常被动。2. 核心细节解析与实操要点2.1 SpringBoot 后端几个容易写歪的关键点第一是登录鉴权。毕设项目最喜欢犯的错误是在每个接口里手动判断 session 里有没有用户。这个做法不是不能用但代码风格很差而且不好扩展。我建议使用 JWTJSON Web Token方案用户登录成功后后端生成一个 token 返回给前端前端后续请求在请求头中带上Authorization: Bearer token后端通过拦截器拦截非白名单接口解析 token 校验用户身份。手写一个 JWT 工具类并不复杂核心代码大概长这样// util/JwtUtil.java public class JwtUtil { private static final String SECRET_KEY office-sale-secret; private static final long EXPIRE_TIME 2 * 60 * 60 * 1000L; // 2小时 public static String generateToken(Integer userId, String username) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes())) .compact(); } public static Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes())) .build() .parseClaimsJws(token) .getBody(); } }第二步是配置拦截器放行哪些路径拦截哪些路径。登录注册接口肯定放行商品列表如果允许游客浏览也可以放行但购物车、订单、推荐这些必须登录。用拦截器的好处是权限控制集中管理而不是散落在每个接口里做重复判断。第二是统一返回结构。我强烈建议项目从一开始就用一个统一的响应体不要有的接口返回 Map有的返回数组有的直接抛异常给前端。定义一个 Result 类包含 code、message、data 三个字段配合全局异常处理器前端 axios 拦截器里只需要判断 code 是否为 200统一弹提示即可。这个设计虽然细节但直接决定了前后端联调的顺畅度。第三是数据库操作层的选型。如果你用的是 MyBatis-Plus那分页查询不要再自己写 limit 计算了用自带的 Page 对象。如果你用原生 MyBatis那特别注意分页参数不要硬编码推荐用 PageHelper 或者自己写一个通用的分页拦截器。办公用品列表肯定是分页展示的这块是高频操作处理不好就是慢 SQL 的起点。2.2 SQL 脚本设计里容易被忽略的地方拿到一个项目不要急着写业务代码先把数据模型想清楚。日常办公用品直售系统我建的这几张表基本够用表名作用核心字段user用户表id, username, password, nickname, avatar, phone, create_timecategory商品分类表id, name, sort, parent_idgoods商品表id, category_id, name, subtitle, price, stock, sales, img, status, detailcart购物车表id, user_id, goods_id, quantity, checked, create_timeorder订单表id, order_no, user_id, total_price, status, create_time, pay_timeorder_item订单明细表id, order_id, goods_id, goods_name, price, quantitybrowse_history用户浏览记录id, user_id, goods_id, view_timerecommend_config推荐配置表id, user_id, category_id, weight, update_time这里说三个关键点。第一密码字段不要明文存储。哪怕只是一个毕设项目也要养成用 MD5 加盐或 BCrypt 加密的习惯。答辩时老师非常喜欢问“你的密码是怎么存储的”一句“BCrypt 加密”就能体现安全意识。第二关于外键。很多教材鼓励建外键但真实项目里我建议在表现层创建逻辑外键也就是在 goods 表里存 category_id 这个普通索引字段但不用 FOREIGN KEY 物理约束。原因很简单物理外键会带来插入顺序问题和级联删除的额外复杂度毕设规模虽然不大但表多起来之后物理外键在各种联调操作中很容易成为阻力。你只需要给常用关联字段建立索引查询性能完全够用。第三SQL 脚本要保证可重复执行。也就是要在建表语句前加上DROP TABLE IF EXISTS这样同一个脚本不管执行多少遍结果都是一致的。数据库初始化时务必用 utf8mb4 字符集直接避免保存中文时出现乱码CREATE DATABASE IF NOT EXISTS office_sale DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE office_sale; DROP TABLE IF EXISTS category; CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort INT DEFAULT 0, parent_id INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;SQL 脚本里的初始化数据也很重要。不要在答辩现场打开一个商品列表是空页面。建议把 8 个左右的商品分类和 30 条左右的商品数据塞进初始化脚本里演示时一打开首页就有内容体验好很多。2.3 Vue 前端与接口对接的核心套路前端这块重点不在于你写了多少炫酷的页面而在于你能不能把接口对接这件事讲明白。axios 实例封装是第一步。避免在每个页面里直接axios.get(...)而是统一创建一个utils/request.js里面做三件事设置 baseURL添加请求拦截器自动携带 token添加响应拦截器统一处理错误码。// utils/request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /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) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } ) export default request然后 api 目录下的每个文件再针对业务封装对应的方法比如goods.js里import request from /utils/request export function getGoodsList(params) { return request.get(/goods/list, { params }) } export function getGoodsDetail(id) { return request.get(/goods/detail/${id}) }这里还想提一下 Vue 的响应式原理。很多同学学 Vue 只停留在会用 template 的程度答辩时被问“Vue 为什么能自动更新页面”就卡住。你至少要能答上来Vue 的核心是通过 Object.defineProperty 拦截对象的属性读写Vue 3 改成 Proxy在访问属性时收集依赖在设置属性时通知依赖更新从而驱动视图渲染。这是 js 深入浅出那套东西里最值得掌握的部分属于面试和答辩都能用上的知识点。路由方面用 vue-router 时一定要做登录守卫。未登录用户访问购物车和订单页直接跳转到登录页router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })3. 实操过程与核心环节实现3.1 环境准备版本匹配决定成败我见过太多同学代码一点问题没有就是环境装不上。Node 版本太高导致 node-sass 报错SpringBoot 版本太高导致 JDK 版本不匹配MySQL 版本差异导致驱动类名老是不对。先把版本这个坑填平你后面就顺了。组件建议版本说明JDK1.8 或 11优先 1.8兼容性最好SpringBoot2.7.x不要直接上 3.x3.x 要求 JDK17Maven3.8.x常用版本Node.js14 或 16Vue 项目推荐避免 node-sass 报错Vue CLI5.x 或 ViteVue3 项目可以直接用 ViteMySQL5.7 或 8.05.7 绝大多数云服务器都有现成方案SpringBoot 版本太高这个问题我特别想多说一句。有些同学为了追新直接用 SpringBoot 3.2结果发现 JDK 17 没装而且很多第三方 starter 还停留在老版本整合起来各种报错。毕设不要追新稳定压倒一切。2.7.x 这一代是最成熟的各种教程资料也最多有问题搜一下全是答案。3.2 初始化数据库SQL 脚本的执行顺序拿到项目源码之后第一步不是启动后端而是先把数据库准备好。打开 Navicat 或者直接用命令行mysql -u root -p然后执行source D:/sql/init.sql;如果你用的是 Navicat直接右键连接选择“运行 SQL 文件”更直观。执行完脚本之后建议检查一下表是否都建出来了USE office_sale; SHOW TABLES;这个检查虽然简单但能避免很多后面踩坑。我遇到过不止一次SQL 脚本执行到一半报错退出后面几行建表和插入数据的语句根本没执行前端都能跑但就是没数据。如果脚本报错不要从头开始找先看报错行号多半是编码问题或者字段类型不一致。这里也顺便提一个热门问题怎么将 SpringBoot jar 反编译成项目。很多时候你拿到的项目是先打包好的 jar 包源码并不齐全。你不需要完全反编译出所有代码只需要把 jar 包拖进 JD-GUI 工具就能看到里边的 class 文件从而还原出实体类、Controller 路径、Mapper 接口这些关键信息。把这些结构信息整理出来再配合接口文档就足够复现整个项目了。这个方法适合参考别人的项目时使用注意千万不能把它当成抄代码的捷径做到能理解结构和接口设计的程度就足够了。3.3 后端启动从改配置到接口自测数据库初始化完成后开始改后端配置文件。打开application.yml核心就改三个地方server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/office_sale?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你自己数据库的密码改完之后直接运行启动类看到最终启动日志打印出类似Tomcat started on port(s): 8080后端就起来了。第二件事是验证接口文档。项目里配套的接口文档如果用了 Swagger启动后直接访问http://localhost:8080/swagger-ui.htmlSpringBoot 2.7 默认是/swagger-ui/index.html或/doc.html。理论上你可以在 swagger 页面上直接点击接口在线测试。这里分享一个小细节。SpringBoot 启动时会打印一个默认的 banner网上有大把的 SpringBoot banner 生成器你可以做一个 ASCII ART 风格的 banner 放进src/main/resources/banner.txt里。虽然不增加任何功能但答辩演示时一启动后端就出现一个摩登的 banner观感会加分不少也可以当作一个小细节讲给老师听。3.4 前端联调把 7 条主流程走通前端启动方式不多说Vue CLI 项目npm run serveVite 项目npm run dev。启动后按照下面这 7 条主流程走一遍每一条都是核心业务注册新用户登录获取 token浏览商品列表查看商品详情通过分类搜索商品把商品加入购物车从购物车创建订单首页查看“为你推荐”每一步都要认真观察接口返回是不是正常。联调过程中最容易出的问题就是字段对不上比如后端返回goodsName前端页面却写的name后端分页返回的是total和records前端写死了rows。所以在开发时接口文档里必须把每一个字段名写清楚前端拿数据时先打印一遍看看结构console.log(res.data)联调不要一次调 7 条。我建议一条一条走每走通一条再走下一条。遇到问题了打开浏览器开发者工具看 Network 面板。前端报 500 说明后端代码有异常后端控制台会打印堆栈前端报 404 看看接口路径是否匹配前端报 401 看看 token 有没有带上。这套排查逻辑虽然基础但能覆盖九成以上问题。4. 常见问题与排查技巧实录4.1 前端跨域问题前端访问后端接口最常见的报错是No Access-Control-Allow-Origin header is present浏览器把请求拦截了。解决办法有两个二选一即可。第一个是后端开启全局跨域配置加一个配置类Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }第二个是在前端配置代理。如果你用的是 Vite在vite.config.js里加这样一段server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } }我个人更推荐第二种因为后端不用处理跨域上线部署时也不容易出问题。注意如果两种方式同时存在会有冲突风险你就选一种固定用。4.2 慢 SQL 与接口卡顿排查如果“为你推荐”这个接口老是转圈或者商品列表页一翻页就卡顿八成不是前端的问题而是 SQL 执行太慢。排查步骤非常简单先用 EXPLAIN 查看执行计划EXPLAIN SELECT * FROM goods WHERE category_id 3 ORDER BY sales DESC LIMIT 10;看结果里的type字段。如果显示ALL就说明是全表扫描这个查询在商品数量变大之后一定会变慢。解决办法是给它建索引ALTER TABLE goods ADD INDEX idx_category_sales (category_id, sales DESC);为什么要用联合索引因为查询条件里面同时用到了分类和销量排序单独给 category_id 建索引只能加速查询但排序还是慢。联合索引让数据库一次就能定位到目标数据。另外关于常见的易 SQL 注入导致性能和安全问题核心原则只有一句话永远不要通过字符串拼接 SQL。MyBatis 里用#{}这个占位符会自动转义参数能防注入。如果一时半会听不懂记住一条判断标准任何让你把用户输入拼到 SQL 字符串里的写法都是错的。4.3 接口文档对不齐联调最大的痛点毕设项目里最浪费时间的事情就是前端照着旧文档写好了后端已经把字段改了。接口文档不是写给别人看的是写给你自己联调用的。不管项目里是否集成 Swagger我建议都在项目根目录放一份 Markdown 版本的接口文档每个接口包含四个部分请求路径、请求方式、请求参数、响应示例。写作格式可以参考下面的例子接口名称商品列表 路径GET /api/goods/list 说明分页获取办公用品列表支持按分类筛选 请求参数 | 参数名 | 类型 | 必填 | 说明 | | page | Integer | 否 | 页码默认1 | | size | Integer | 否 | 每页条数默认10 | | categoryId | Integer | 否 | 分类ID | | keyword | String | 否 | 商品名称模糊搜索 | 响应示例 { code: 200, message: success, data: { total: 30, records: [ { id: 1, categoryId: 3, name: 中性笔, price: 20.5, stock: 100, sales: 30 } ] } }一个接口一个接口写清楚写完就同步。凡是联调时出现“前端拿到 undefined”这类问题你回头对照文档立马就能发现是后端返回字段和文档不一致还是前端取错名字。4.4 答辩 QA 与版本兼容问题最后这节虽然不属于运行问题但答辩时被问的概率非常非常高。我提前列几个老师最爱问的问题吧。第一个问题是“你的推荐系统是怎么实现的”。如果你做的版本比较简单比如基于商品分类热度推荐或者热门商品推荐就直接说目前是基于用户收藏记录和商品销售热度计算每个分类的权重结合用户最近浏览内容来生成推荐列表。不要试图吹一个没做的协同过滤算法老师深问两句就穿帮了。第二个问题是“SpringBoot 自动配置的原理”。解释清楚三个点自动配置类的条件注解ConditionalOnClass、配置属性绑定、spring boot starter 的加载入口。这套东西背熟即使没实现过也有理论支撑。第三个问题是“你对系统做了哪些优化”。你可以从三个方向谈数据库加索引优化查询、JWT 替代 session 减少服务器状态存储压力、接口文档驱动前后端分离开发。哪怕只是做了索引也是真实的优化项。版本兼容问题再补一刀有些同学用 SpringBoot 2.7 会遇到跟 JDK 17 的兼容性问题比如IllegalArgumentException: Unsupported class file major version这就是你用 JDK17 编译、但 SpringBoot 的 CGLIB 代理还停留在老版本导致的。解决办法就是 JDK 版本和 SpringBoot 版本对齐别一个高一个低。5. 扩展建议与个人体会关于这套系统怎么扩展简单聊一下。日常办公用品直售推荐系统的下一个阶段比较顺的升级方向有两个。一个是在前端加一个基于用户协同过滤的推荐接口思路非常简单找出与当前用户购买行为最接近的其他用户把他们买过的而你没买过的商品推荐给你。另一个是把商品图片存储从本地目录迁移到 MinIO 这样的对象存储服务这也是最近很多人问的“minio 加入到 springboot”常见需求。MinIO 的 S3 API 很容易整合后端配置一个 MinioClient 的 Bean上传文件时生成预签名 URL前端就能直接展示图片了。这两个扩展方向都很有代表性做完之后项目可以从“毕设水平”提升到“简历项目水平”。最后说一点个人体会。做毕设项目的时候最大的敌人不是复杂度而是拖延和盲目。如果你一上来就想着写一个天衣无缝的推荐系统、做一个跟大厂一样的前端交互这个项目大概率会烂尾。先跑通最核心的链路用户注册、登录、看商品、加购物车、下单、出推荐结果。全链路通了之后再去补那些加分项什么 banner、图片上传、订单状态机每加一个都是实实在在的成果。我做这套项目时最深的感受是完整跑通一遍的价值远大于零散地读十篇教程。跟着接口文档走一遍流程把数据库从这个表跳到那个表再把前端页面和数据关联起来你对 Java Web 这套技术栈的理解会瞬间变得具体。如果你拿到的项目还没有跑通或者正在被某个环境问题卡住看一下这篇里对应的问题一般都能找到答案。整个项目跑通的那一刻你再看这份源码会发现它不再是别人的代码而是你自己能解释清楚、能继续改动的设计。
返回列表