“又到一年毕业设计选题的‘纠结季’”。每次刷到这类话题总能看到满屏的图书商城、疫情打卡系统、校园二手交易。不是说这些题目不行而是答辩老师早就看腻了一问“你的项目有什么亮点”学生自己都答不出个所以然。今天聊的这套SpringBootVue日常办公用品直售推荐系统是我反复整理过的一套毕设方案项目完整度很高源码、SQL脚本、接口文档三件套齐全非常适合Java Web方向的同学参考。这套平台的核心切入点有两个一是“直售”业务链路短用户注册登录、浏览商品、加购下单、支付流程模拟、后台管理一套标准的电商闭环二是“推荐”不是花里胡哨的大模型推荐而是基于用户行为标签加权打分的一套轻量推荐逻辑既能给论文凑出技术亮点又能真真切切跑出“猜你喜欢”的效果。不管你是想直接拿来做毕设还是打算照着这个思路重新写一套这篇文章都值得看完。1. 为什么选“办公用品直售推荐”当毕设题目1.1 垂直场景比通用商城更容易做出辨识度图书商城、通用商城这种题目最大的问题不是难度而是“雷同”。十个选电商方向的人八个是做图书剩下两个做二手。办公用品这个场景天然有一层“企业采购”“日常耗材”的滤镜商品品类丰富——文具、打印耗材、收纳、办公设备、茶水间用品——每类商品都可以打上细分的标签这让推荐功能有了落地的土壤。而且“直售”这两个字很关键。它表示平台方自己对货品负责不像淘宝那种商家入驻模式需要设计店铺体系、商家后台、三级分销业务边界非常清晰。对于一个本科生来说一周能理清需求两周能做完核心功能这是最理想的工作量。1.2 推荐功能是论文里最好写的“技术亮点”很多人一听推荐系统就慌觉得那是算法工程师干的事。实际上在毕设这个体量下完全不需要协同过滤、矩阵分解这类东西。用“用户行为商品标签”打分排序已经足够支撑起一个完整的推荐模块。这套系统的推荐思路很简单系统记录用户浏览、加购、下单三类行为为每类行为分配不同的权重再提取用户近期行为涉及的标签频次给候选商品打标签匹配分。再加上新品补偿、热门补偿、库存过滤最终推出来一个排序列表。这个方案讲得清楚、代码写得出来、演示效果也直观答辩老师挑不出大毛病。等你把协同过滤的改进方向在论文里篇幅一写亮点直接拉满。1.3 一个平台覆盖了Java Web的全部核心考点我粗略数了一下这套系统的技术覆盖点前后端分离架构、RESTful接口设计、JWT无状态登录、SpringBoot自动配置、MyBatis数据持久、MySQL事务与索引、Vue组件化开发、Axios请求封装、跨域处理、接口文档编写。说句不夸张的答辩现场老师从这几个方向随便问你都能掏出对应代码讲两分钟。2. 技术栈选型与整体模块拆分2.1 后端这套组合为什么至今还很能打我见过很多同学习惯用最新版本SpringBoot 3.x、JDK 17、Vue3TS全套上。实话说做毕设真没必要追新。我整理这套项目时后端用的是SpringBoot 2.7.x JDK 8这个组合最大的优势是“兼容性极好”网上搜到的报错解决方案基本都是这个版本区间遇到问题不容易卡死MyBatis-Plus 3.5.x对单表CRUD的简化力度非常大商品、订单、用户这些基础表基本不用写SQLJava生态里大量遗留代码、工具类都跑在JDK8上毕业设计不涉及高并发性能调优没有必要上虚拟线程那套新特性。鉴权这块用了JWT具体实现是jjwt库配合一个HandlerInterceptor拦截器统一校验。为什么不用Session因为前后端分离之后Session天然不适合跨域场景而且JWT的无状态特性在答辩时更容易讲出设计感。2.2 前端为什么用Vue2而不是Vue3这个问题肯定有人问。答案是Vue2 Element-UI这套组合在中文技术社区的教程存量太大了。毕设不是企业级项目讲究的是快速出活。Vue2的Options API对新手非常友好data、methods、computed写起来直观Element-UI的表单、表格、弹窗组件直接拿来用无论是用户端的商品卡片还是管理端的订单表格半天时间就能搭出有模有样的界面。Vue3就算用Composition API逻辑上更灵活但对一个毕业设计来说这种灵活反而是负担。2.3 系统模块怎么拆这套系统的模块划分如下模块使用者核心功能对应后端Controller用户认证所有用户注册、登录、个人信息维护AuthController, UserController商品直售普通用户商品列表、详情、分类筛选、搜索ProductController, CategoryController智能推荐普通用户首页推荐流、热门商品、相似推荐RecommendController购物车与订单普通用户购物车增删改查、下单、订单列表、订单详情CartController, OrderController行为采集普通用户浏览、加购、购买行为记录BehaviorController后台管理管理员商品管理、分类管理、订单处理、用户管理、推荐配置AdminProductController, AdminOrderController等数据统计管理员销量统计、分类占比、用户增长StatsController这个拆分逻辑对应着“前台用户端 后台管理端”两套界面体系但后端共用同一套SpringBoot服务。很多毕设项目喜欢把代码拆成前后端两个仓库其实对于一个学生项目为了保证演示方便我更推荐前端Vue项目和后端Java项目物理分离但最终部署时打包成一个可执行jar。3. 后端核心链路从登录鉴权到下单扣库存3.1 JWT登录与统一鉴权先看登录流程用户提交用户名密码后端校验通过后签发一个JWT Token前端把它存在localStorage里之后每次请求都在请求头带上Authorization: Bearer xxx。PostMapping(/login) public ResultInfoString login(RequestBody LoginDTO dto) { User user userService.login(dto.getUsername(), dto.getPassword()); if (user null) { return ResultInfo.error(用户名或密码错误); } String token JwtUtil.createToken(user.getId(), user.getRole()); return ResultInfo.success(token); }核心校验逻辑放在拦截器里注意要放行登录注册接口、商品查询接口其余接口一律校验TokenOverride public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equals(OPTIONS)) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new BizException(401, 未登录或登录已过期); } // 解析token将userId放入request attribute供后续使用 Claims claims JwtUtil.parseToken(token.substring(7)); request.setAttribute(userId, claims.get(userId)); return true; }这里有个小细节拦截器只是负责“认不认这个Token”具体的“用户是否存在、权限是否足够”得在Service层再做一次校验。很多人只写了拦截器结果把已经删除的用户Token也能放进来这就是安全意识不足。3.2 商品直售模块分页查询必须处理好三个条件商品列表是用户端的门面接口设计上要支持关键词模糊搜索、分类筛选、上下架状态过滤、价格区间、分页。MyBatis-Plus的LambdaQueryWrapper可以很轻松搞定LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.like(Product::getName, keyword); } if (categoryId ! null) { wrapper.eq(Product::getCategoryId, categoryId); } wrapper.eq(Product::getStatus, 1); // 只展示已上架 wrapper.orderByDesc(Product::getSales); PageProduct page productMapper.selectPage(new Page(pageNum, pageSize), wrapper);这里建议大家注意一个点管理端的商品列表和用户端的商品列表要分开接口。用户端永远只查已上架商品管理端需要看到全部商品包括下架的。我见过有同学图省事用一个接口前端控制状态结果用户直接改参数就能看到管理端的停售商品数据。3.3 下单与库存扣减事务和行锁缺一不可下单是电商类项目最容易暴露问题的环节。很多初写代码的同学先查库存if判断库存充足再减库存最后插入订单——这套逻辑在并发环境下会出大问题。两个请求同时查到库存还有1件双双判断“充足”结果两个都下单成功库存变成-1。正确的做法是把“判断库存”和“扣减库存”合并成一条原子SQL利用MySQL的行锁同一时间只有一条更新能成功Transactional(rollbackFor Exception.class) public OrderVO createOrder(Long userId, ListCartItemDTO items) { // 1. 校验购物车数据 // 2. 遍历扣减库存 for (CartItemDTO item : items) { int affected productMapper.deductStock(item.getProductId(), item.getQuantity()); if (affected 0) { throw new BizException(商品库存不足 item.getProductName()); } } // 3. 创建订单主表 // 4. 批量插入订单明细 // 5. 清空购物车写入购买行为 // 6. 返回订单详情 }对应的SQL语句是UPDATE product SET stock stock - #{count}, sales sales #{count} WHERE id #{productId} AND stock #{count}受影响的记录数为0说明库存不足或者商品下架直接抛异常回滚事务。这种写法在高并发下依然成立因为数据库行锁保证了并发更新串行执行。答辩现场被问“如何防止超卖”这个答案绝对够用。3.4 统一返回体和全局异常处理前后端分离项目最忌讳的就是每个接口返回结构都不一样。这套系统的所有接口统一返回ResultInfoT结构code状态码200成功401未登录500业务异常message提示信息data业务数据配合一个全局异常处理器RestControllerAdvice业务中任何地方抛出BizException前端都能拿到格式一致的错误信息。刚开始写代码的同学可能觉得麻烦但这套规范能让你少踩很多联调阶段的坑。4. 推荐逻辑实战标签加权排序也能做出“像样”的推荐效果4.1 推荐逻辑的完整链路这套系统推荐的实现路径是用户行为采集 - 行为权重计算 - 标签提取与候选召回 - 标签匹配打分 - 排序与过滤。用户发生的每一条关键行为都会写入行为表包括商品ID、行为类型、时间戳。行为类型权重在设计上是这样分配的行为类型权重含义browse1浏览商品详情页表示初步兴趣addCart3加入购物车购买意向明显purchase5实际下单兴趣最强烈同时商品表里维护了一个tags字段用逗号分隔例如一份商品“A4复印纸”的标签是“办公,耗材,纸张”。后台管理商品时可以随时调整标签。4.2 用标签匹配给候选商品打分当用户进入首页请求推荐流时后端做的事情分为三步第一步查用户最近30天的行为记录按行为类型加权计算每个标签的总得分。代码逻辑参考MapString, Double tagScoreMap new HashMap(); // weightMap: browse1, addCart3, purchase5 for (Behavior behavior : recentBehaviors) { for (String tag : productService.getProductTags(behavior.getProductId())) { tagScoreMap.merge(tag, weightMap.get(behavior.getType()), Double::sum); } }第二步遍历候选商品排除用户已购商品、下架商品对每个商品打标签匹配分。每命中一个用户高偏好标签就累加对应的标签得分没有命中的标签不加分。第三步在标签匹配分的基础上叠加“热门商品基础分”和“新品补偿分”再乘一个库存权重系数。这一步是为了避免所有的推荐都被几款热门商品霸占新品和长尾商品也能有机会露出。最终按照总分倒序取前20条作为推荐结果。整套逻辑在Java侧做循环计算对于几百条商品数据毫秒级返回完全够用。4.3 冷启动场景登录前后完全两种体验新用户没有行为数据标签匹配就没法做这时候推荐接口自动降级为“热门商品榜”按照销量和浏览量倒序。用户在未登录状态下也可以浏览商品但行为采集在前端判断到用户已登录时才会上报避免脏数据污染推荐模型。这种行为数据的新老分离是很多同学容易忽略的点。我看到过某个项目把所有匿名浏览都记录进行为表结果推荐出来的东西乱七八糟就是因为没做登录态判断。4.4 答辩时怎么把推荐算法讲清楚给老师讲推荐模块的时候一定要强调设计思路先明确“用户兴趣由行为表达”再解释“标签是商品和用户之间的桥梁”最后说明“权重可配置化”——后台管理模块专门有一个推荐参数配置页可以调整行为权重、热门补偿分、新品补偿分。这一个设计细节直接体现了你具备“参数可调、系统可运维”的工程意识。5. 前端页面的实现要点从Element-UI到Axios封装5.1 页面路由清单用户端和管理端是两套独立路由整体页面清单如下角色路由页面核心交互未登录用户/login, /register登录注册表单校验、Token存储普通用户/首页推荐流推荐商品卡片、热门榜单普通用户/product/list商品列表分类联动、关键词搜索普通用户/product/detail/:id商品详情加入购物车、立即购买、相关推荐普通用户/cart购物车数量修改、选中计算普通用户/order/confirm确认订单地址管理、提交订单普通用户/order/list我的订单订单状态筛选管理员/admin/dashboard数据看板ECharts统计图表管理员/admin/product商品管理CRUD、上下架、标签编辑管理员/admin/order订单管理发货、退款处理管理员/admin/recommend推荐配置行为权重参数设置5.2 Axios请求封装每一处请求都自动带Token说个真实经验很多同学把携带Token的逻辑写在每一个页面的方法里写了一百遍冗余代码改一个后端接口前缀还要全局搜索替换。这套项目里只封装一次Axios实例所有网络请求统一走它import axios from axios 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 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(登录已过期)) } if (res.code ! 200) { Message.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, error { Message.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } )这段代码的价值在于所有页面调接口只需要关心业务数据不需要重复处理Token、错误弹窗和登录跳转。5.3 本地开发跨域代理配置前后端分离联调阶段跨域是非常经典的坑。前端跑在8080端口后端跑在8080端口浏览器直接请求后端接口会被CORS拦截。解决方式有两种后端加CORS配置或者前端用开发服务器做代理转发。我推荐用代理转发方案配置在vue.config.js里module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } }这里的关键点是路径规划前端请求统一加/api前缀代理转发时去掉前缀再转发给后端。这样即使以后部署到生产环境用Nginx做反向代理时也能复用同一套规则不需要改前端代码。5.4 购物车数量和用户信息的全局状态购物车数量角标这种数据用户不可能十秒不刷新就能忘记状态所以一定不能靠组件通信传来传去要用Vuex或Pinia做全局状态管理。我在这套项目里把“用户信息、购物车数量、待支付订单数量”这几个高频使用的数据放进了Vuex每次登录后拉取一次加购、下单成功后重新计算。前端状态一致了后端数据一次查询到位交互体验比从零开发要自然很多。6. SQL脚本里的关键设计建表、数据脚本与常见坑6.1 核心表结构与表关系说明这套系统的SQL脚本里一共十来张表环环相扣我挑核心的几张开个头表名功能关键字段sys_user系统用户普通用户管理员id, username, password, role, avatarproduct_category商品分类id, name, parent_idproduct商品表id, category_id, name, price, stock, tags, sales, status, imagebehavior用户行为表id, user_id, product_id, type, create_timecart购物车表id, user_id, product_id, quantitytrade_order订单主表id, order_no, user_id, total_amount, status, create_timetrade_order_item订单明细表id, order_id, product_id, product_name, price, quantityrecommend_config推荐参数配置表id, behavior_type, weight设计表的时候我最想强调的一点是不要建物理外键但要在逻辑上有外键关系比如trade_order_item.order_id对应trade_order.idcart.user_id对应sys_user.id。物理外键在单机演示时不会有问题但未来如果要分库分表或者做数据迁移物理外键就是灾难。很多企业开发规范里默认禁用物理外键这可以作为答辩时的一个加分回答。6.2 订单状态流转设计订单状态我没用孤立的字符串而是用整数状态码加字典解释状态码含义可操作动作0待付款取消订单、去支付1待发货管理员发货2已发货用户确认收货3已完成申请售后/评价4已取消无5退款中管理员同意/拒绝退款状态流转必须由后端代码控制比如只有状态为0的订单才能执行取消操作否则返回“当前订单状态不支持该操作”。这种设计虽然简单但能把边界条件卡得很稳。6.3 SQL脚本导入的三大坑从零导入SQL脚本到MySQL时最常见的坑是这三个第一是字符集。建表语句必须显式声明CHARSETutf8mb4否则中文商品名导入后直接变乱码。utf8mb4比utf8多了对Emoji的支持宁可一步到位。第二是执行顺序。如果脚本里不是按主表在先、从表在后的顺序导出的带物理外键的表就会导入失败。这类问题排查起来很费时间不如干脆不建物理外键。第三是时间字段默认值。很多MySQL的datetime默认值在5.7和8.0版本表现不同。设置create_time DATETIME DEFAULT CURRENT_TIMESTAMP在8.0下没问题但在某些低版本下会报错。稳妥做法是Java代码插入数据时显式设置时间字段。6.4 演示数据怎么造才像模像样一套空数据库演示推荐功能的时候什么效果都跑不出来。我建议用脚本批量生成演示数据50个左右的测试用户200个商品每个商品3到5个标签然后模拟每个用户随机产生几十条浏览、加购、购买行为。推荐接口的Demo效果基本上一跑一个准真实感满分。7. 接口文档、环境搭建与部署排错7.1 接口文档到底要写到什么程度很多同学拿到项目之后觉得接口文档就是鸡肋。实际上对于毕设来说接口文档是毕业论文“系统详细设计”那一章的现成素材答辩时直接翻给老师看比空口讲设计有说服力得多。一个合格的接口文档至少要包含这些内容接口名、请求URL、请求方式、请求参数说明参数名、类型、是否必填、返回结果示例、错误码业务含义。如果能再把接口分成“用户端接口”“管理端接口”两个分类读者看起来会更清晰。7.2 本地运行的全流程和环境配置这套项目的环境要求如下组件推荐版本说明JDK1.8高版本需调整部分依赖Maven3.6国内镜像源加速依赖下载Node.js14 LTS 或 16 LTSVue2项目不要用太高版本MySQL5.7 或 8.0两者都支持注意驱动版本启动步骤非常简单先创建数据库导入SQL脚本再修改后端application.yml里的数据库账号密码启动后端SpringBoot应用进入前端目录执行cnpm install或npm install再执行npm run serve浏览器访问3000端口即可。7.3 部署排错最常见的几个实测坑按真实使用反馈下面这几个坑出现的频率最高我按排查优先级列出来现象排查点解决方案后端启动后端口被占用检查8080端口占用杀掉占用进程或修改server.port数据库连接报错 “Public Key Retrieval is not allowed”MySQL8.0连接串url末尾加allowPublicKeyRetrievaltrue数据库连接报错 “The server time zone”时区问题url中加serverTimezoneAsia/Shanghai前端请求报404代理路径和后端context-path统一约定 /api 前缀转发规则上传的商品图片不显示静态资源映射在配置里设置资源映射路径指向本地磁盘目录尤其是上传图片这个问题很多人喜欢把图片存到项目的resources/static/upload目录但打包成jar之后写入jar内部的文件再想访问就特别别扭。更合理的方案是上传到本地磁盘的固定目录再通过配置类把/upload/**映射到那个物理路径。运行环境变了只需要改配置代码不用动。7.4 前后端最终打包合并成单jar毕设答辩现场最怕的是演示环境网络出问题前端依赖装不上、后端服务起不来。稳妥的方案是前端执行npm run build生成dist目录把dist里的内容复制到后端src/main/resources/static下然后用mvn package打成可执行jar包。启动jar之后直接访问http://localhost:8080SpringBoot会自动把前端静态资源一起托管单进程部署演示零压力。这个方案需要额外注意一点Vue Router要使用hash模式而不是history模式否则刷新二级页面时会出现404。8. 答辩现场被问得最多的几个问题和应对思路8.1 高频问题与回答要点老师常问的问题回答要点延伸话术为什么用JWT不用Session前后端分离、跨域友好、天然适合集群扩展生产环境会用Redis做Token黑名单实现主动下线怎么防止库存超卖原子条件更新事务回滚高并发可以用乐观锁更极端可以引入MQ串行化推荐算法的原理是什么用户行为加权标签匹配补偿分后续可以改进成基于物品的协同过滤前后端是怎么通信的Axios封装成统一实例走Restful接口JSON格式接口文档里每个接口都有请求和返回结构定义为什么用MyBatis-Plus单表CRUD零SQL复杂SQL手写XML对比MyBatis的效率提升点你的项目里哪个部分最难订单事务一致性把下单那段代码和事务注解圈出来讲8.2 被问“后续怎么优化”的高阶技巧如果老师问“还有什么可以改进的地方”千万别答不知道。可以这样接第一库存扣减可以引入Redis缓存预扣减降低数据库压力第二推荐算法可以升级成协同过滤用物品间相似度矩阵做实时推荐第三文件存储可以接入对象存储把本地磁盘换成云端桶第四支付模块可以对接真实第三方支付网关把模拟支付换成真实支付流程。这四条只要说出来任何一个有实际经验的评委都会觉得你确实思考过系统的演进方向。8.3 论文里怎么把项目讲得有深度写论文的时候建议把第4章的推荐模块作为系统详细设计的重点数据模型、权重公式、算法伪代码画出来就占了不少版面。答辩PPT里放一张推荐流程图加上几张不同行为数据下的推荐效果截图。相比那些满页贴登录注册代码的同学你的工作量饱和度在这几个技术点上会被明显感受到。最后再说几句掏心窝的话我见过很多同学拿到项目源码后拼命背代码结果答辩时老师换个角度问一句就露馅了。我个人的体会是这类毕设项目的价值不在“代码跑通了”而在你是不是真的吃透了里面的每一条SQL、每一个设计决策背后的原因。所以最后分享一个小建议拿到任何一套类似的SpringBootVue项目不要急着改项目名交差。先按照我上面讲的链路从登录鉴权到下单扣库存手动改写一遍核心代码再把推荐模块的权重表改成你自定义的参数然后重新造一批演示数据。这个过程走完不仅答辩稳如磐石面试官问“你的毕设做了什么”时你也能讲得底气十足。Java Web方向不缺从零到一搭框架的能力缺的是能说清楚“每个模块为什么这么设计”的实战理解。