基于SpringBoot+Vue3的办公用品直售推荐系统设计与实践

📅 发布时间:2026/10/9 6:06:36
基于SpringBoot+Vue3的办公用品直售推荐系统设计与实践
办公用品采购这件事听起来简单真正做过系统的人才知道里面有多坑。尤其是“直售推荐”这种组合你一边要保证商品展示、下单、库存这些基础链路跑得稳一边还得让用户觉得“系统懂我”能根据他的行为把该推的商品推到他眼皮底下。今天就拿这套基于 Java SpringBoot Vue3 MyBatis 的日常办公用品直售推荐系统源码完整拆一遍它的设计思路和落地细节。这套系统前后端分离数据层用 MySQL适合正在做毕设、想转型全栈、或者公司内部要搭一套小型采购平台的开发者参考。先把它是什么说清楚。这是一套完整的可运行项目后端负责商品管理、用户管理、订单流转和推荐计算前端是 Vue3 构建的管理端加用户端界面替你做掉了前后端交互、接口鉴权、数据可视化这些脏活累活。和一般那种把前后端代码混在一起的单体项目不同它是典型的分离架构接手就能当脚手架用也能在此基础上做二次开发。推荐功能是这套系统里最值钱的部分它不是那种随便写个“猜你喜欢”的摆设而是基于用户行为日志做的协同过滤推荐。这里用了 MySQL 存储埋点数据配合 MyBatis 访问层做计算没有引入乱七八糟的搜索服务和大数据组件成本低效果也足够中小型系统用。下面我就从架构选型、数据库设计、推荐算法实现、前后端联调、实操部署几个维度把整个项目从零到一拆干净。1. 需求拆解直售系统加推荐模块核心矛盾在哪先说需求层面。直售和推荐听起来是两件事其实它们共享同一个数据底座用户、商品、订单。很多新手会把推荐系统当成一个独立模块等基础功能做完了才回头补这就容易踩坑。因为推荐系统依赖的是用户行为数据而行为数据必须从浏览、加购、下单这些动作里持续采集你如果前期没设计埋点后面再怎么补也补不出真实数据。1.1 直售系统的核心诉求直售模式跟前几年流行的多商户平台不一样它没有店铺概念所有商品是平台自营的价格统一、库存统一、售后也统一。这对数据模型的影响很大商品表不需要冗余商家的字段订单表也不需要拆出子订单分摊给多个卖家。我这个项目里订单主表加明细表两层结构就够了所有关联查询都围绕一个 merchant 维度展开。这样设计的好处是SQL 简单事务控制也简单尤其适合后续要做推荐计算的场景——你不需要在多租户数据里做隔离推荐算法可以直接对全量用户行为建模。办公用品这个品类本身也有特点。它的复购性强比如A4纸、中性笔、打印硒鼓这些是消耗品用户每隔一段时间就会重新下单但它的客单价通常不高用户决策路径短不太会像买数码产品那样反复比价。这意味着推荐的时效性比精准度更重要用户今天买了打印纸一周后他大概率需要一个新硒鼓而不是一支笔。基于这个判断合理的设计是把“行为权重”往近期数据倾斜关联商品推相似品类的效果要好于推热门品。1.2 推荐模块该放在哪一层去实现这是个很有争议的点。有人把推荐放在前端做用户登录后请求一个推荐接口后端临时跑SQL算完返回。这样做性能往往撑不住因为协同过滤要扫描大量用户行为记录同步算会拖垮接口。还有人把推荐逻辑放中间件里引入 Redis 和 MQ用异步任务处理这套对于大流量平台是对的但对中小型项目来说引入的运维成本远大于收益。我的选择是用 MySQL 存数据用 Spring Boot 的定时任务做离线计算把物品相似度结果提前算好写入一张单独的推荐结果表。用户访问“猜你喜欢”接口时直接读表返回秒出。只有当用户行为足够多、触发重新计算时才重跑一次定时任务。这么做有三个好处第一是不打断线上请求第二是逻辑简单第三是可以直接在MySQL里调试推荐结果排查问题非常直观。2. 技术选型SpringBoot、Vue3、MyBatis到底怎么分工这套系统的技术栈不是随便拼的每一层都有明确的职责划分。后端用 Spring Boot它负责提供RESTful接口、事务管理、权限控制和定时任务调度。前端用 Vue3它负责页面渲染、状态管理、路由跳转和交互反馈。MyBatis 作为持久层框架封装所有SQL逻辑。整个链路是典型的请求-响应模型Vue3 发请求Spring Boot 接收处理MyBatis 去 MySQL 做增删改查再把结果一步步返回给用户。2.1 后端为什么选 Spring Boot 而不是 SSM 或 SpringCloudSpring Boot 对中小型系统来说是性价比最高的选择。它内嵌Tomcat打包成jar就能跑不需要额外配置服务器自动装配帮我省掉了一半的配置代码Actuator 做健康检查非常方便。如果你的团队只有两三个人Spring Cloud 那套服务发现、配置中心、网关完全是用不上的徒增维护负担。Spring Boot 加上它的生态比如validation参数校验、全局异常处理、定时任务注解已经能覆盖这个项目百分之百的需求。这里我想重点说一下版本问题。有人用Spring Boot 3.x 配 MyBatis 时候踩了坑原因是 Spring Boot 3 的 jakarta 命名空间跟 2.x 的 javax 不兼容。这个项目用的是稳定的 2.7.x 版本配合 MyBatis Spring Boot Starter 2.3.x不会有这些兼容性烦恼。如果你喜欢尝鲜想用 Spring Boot 3 做那就要确定三件事JDK 版本至少17MyBatis Starter 要用3.0以上Tomcat 相关配置路径也要同步调整不然启动就会报 ClassNotFound。2.2 前端为什么要押注 Vue3 组合式APIVue3 相对于 Vue2 最大的变化是组合式API它让逻辑复用变得非常直接。以前 Vue2 你要抽一个公共逻辑就得写 Mixin问题在于 Mixin 的变量来源不清晰项目一大了之后根本不知道这个 data 里的字段是哪个 Mixin 注入的。Vue3 的 setup ref/reactive 把逻辑按功能聚合代码组织方式从“按选项类型分”变成了“按业务模块分”维护体验完全不一样。这个项目的前端部分我采用了 setup 语法糖配合 Vite 作为构建工具。Vite 开发环境是 Esbuild 预构建依赖冷启动速度比 Webpack 快一个量级。平时开发时前端跑 5173 端口后端跑 8080 端口通过 Vite 的 proxy 配置把 /api 前缀的请求代理到后端地址完美规避跨域问题。这一点比在 Spring Boot 里全局配置 CORS 要更合理因为跨域处理是前端开发环境的事上线后 Nginx 反向代理会接管这个职责。2.3 MyBatis: 为什么不用 JPA 或 MyBatis-PlusJPA 对复杂查询的支持是个痛尤其是这个项目里推荐相关的多表关联和统计查询用 JPA 写原生SQL的话方法命名规则就很难看。MyBatis 就自由得多SQL 就在 XML 文件里想怎么写怎么写DBA 朋友来看代码也很亲切。至于为什么不用 MyBatis-Plus我承认它在单表 CRUD 上确实方便少写很多XML但这个项目里但凡核心点都集中在自定义查询上Plus 的封装反而多余还需要额外处理主键策略、逻辑删除这些配置。直接原生 MyBatis控制力最强排查问题时眼见的SQL是最靠谱的。3. 数据库建模一张用户行为表怎么支撑整个推荐引擎整个项目的核心数据表共九张分别为用户表、商品类别表、商品表、购物车表、订单表、订单明细表、收货地址表、操作日志表、物品相似度表。其中用户行为数据没有单独建表而是直接复用操作日志表和订单表这算是我做的一个简化设计省了一张大宽表。3.1 商品表中的关键字段设计商品表是最基础的数据源推荐计算里要用到商品类别、价格、上架状态和封面图。我的表设计里每个商品有一个 category_id 外键关联到商品类别表。设计主键时没有用自增id而是用了雪花算法生成的 Long 型 id。原因是订单明细和购物车都要引用商品id自增id在数据迁移和合并场景里容易出现冲突雪花id则没有这个问题。MyBatis 的插入语句里需要手动给 id 字段赋值我封装了一个 IdWorker 工具类在 service 层调 insert 之前生成。商品表里有个字段容易忽略它就是 sale_count 销售数量。这个字段的本意是减少统计压力但它在推荐算法里也有大用处当用户行为数据很少时sale_count 可以当作热门物品的排序依据用来做冷启动推荐兜底。所以录入商品时这个字段要尽量真实哪怕早期是模拟数据也要符合帕累托分布——少部分商品销量高大部分商品销量低这样推荐结果才不至于清一色推爆款。3.2 用户行为埋点不只记录“买了什么”要算协同过滤只靠订单数据是不够的还得知道用户看了什么、收藏了什么、往购物车里加了什么。我的做法是建了一张 operation_log 表包含 user_id、item_id、action_type、create_time 四个核心字段。action_type 是枚举1 代表浏览2 代表加购3 代表下单4 代表收藏。同时用一张 dict 表记录行为权重浏览记为1分加购记为4分收藏记为3分下单记为8分。这个权重不是拍脑袋写的电商行业一般认为下单行为比浏览行为更能反映真实偏好收藏和加购介于两者之间。埋点的时机放在了前端。商品详情页的 onMounted 钩子函数里发一个 POST /api/log/view 请求后端异步接收不做复杂校验直接插库。这里要注意埋点请求的响应时间不能拖累主流程。我用的是 Spring Boot 的 Async 注解把插入逻辑放到线程池异步执行前端不用等这个请求返回。如果你不改异步机制每个商品详情页多一次串行请求体验会明显变差。3.3 物品相似度表的计算逻辑与更新策略这张表是推荐系统的“预计算结果”核心字段是 item_id_a、item_id_b、similarity、update_time。数据来源是定时任务我设置每十分钟跑一次。计算方式是基于物品的协同过滤具体来说遍历所有用户的行为记录构建一个“用户-物品”评分矩阵然后通过余弦相似度公式计算物品两两之间的相似程度。举个例子用户A浏览了商品1和商品2用户B浏览了商品2和商品3。矩阵里商品1和商品2在用户A这里产生了共现商品2和商品3在用户B这里产生了共现。相似度计算后会得到一个数值值越大说明两个商品经常被同一批用户同时关注。这个逻辑用Java实现并不复杂两层循环加一个Map即可但要注意控制遍历范围物品数量超过500时全量两两计算会导致内存飙升O(n平方)的空间复杂度是撑不住的。我加了过滤条件只统计行为次数超过5的商品相当于一个基于频率的剪枝策略实测效果变化很小性能提升巨大。4. 推荐模块落地从算法理论到能跑的SQL和代码这一部分是整个项目里最有含金量的地方也是面试官最感兴趣的地方。我会从算法、SQL、接口三层逐一说明。4.1 基于物品的协同过滤为什么选它而不是基于用户的协同过滤有两类基于用户的UserCF和基于物品的ItemCF。UserCF 的核心是找到与当前用户兴趣相似的其他用户把那些用户喜欢的物品推荐给我。听起来很合理但它有两个致命缺点第一是用户数量远大于物品数量用户相似度的计算量更大第二是推荐结果的可解释性差用户不知道为什么系统推荐了这个东西容易产生“惊悚感”。ItemCF 恰好规避了这两个问题物品之间的相似度相对稳定可以离线算推荐时直接查表速度快解释也简单你看“买了A的人还买了B”这个文案用户很好理解。办公用品系统的场景也适合ItemCF。因为消耗品的复购周期是有规律的用户买过文件夹大概率下次会买档案盒这种“物与物”的关联性很强。ItemCF 捕捉到的正是这种模式而不是依赖用户群体的人口属性。4.2 核心SQL从行为日志中提取评分矩阵获取用户-物品评分矩阵我用了一条简化的SQLSELECT user_id, item_id, SUM(weight) AS score FROM operation_log WHERE action_type ! 1 GROUP BY user_id, item_id HAVING SUM(weight) 0这条SQL的作用是把加购、下单、收藏三类行为聚合成分数这条SQL的结果会作为协同过滤的输入。过滤掉浏览量action_type ! 1是刻意的因为浏览行为噪声太大一个用户可能因为误点进入某个商品详情页但绝不会误点加购按钮。如果浏览也算高分推荐结果的准确率会被严重稀释。拿到评分矩阵后我在Java里用 MapLong, MapLong, Double 结构存储外层的key是用户id内层的key是物品idvalue是加权分数。然后遍历所有物品对通过余弦相似度公式计算相似性。核心代码摘录如下public double cosineSimilarity(MapLong, Double itemVectorA, MapLong, Double itemVectorB) { SetLong commonUsers new HashSet(itemVectorA.keySet()); commonUsers.retainAll(itemVectorB.keySet()); if (commonUsers.isEmpty()) { return 0.0; } double dotProduct 0; double normA 0; double normB 0; for (double score : itemVectorA.values()) { normA Math.pow(score, 2); } for (double score : itemVectorB.values()) { normB Math.pow(score, 2); } for (Long userId : commonUsers) { dotProduct itemVectorA.get(userId) * itemVectorB.get(userId); } return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); }在工程上有一个巧妙的优化就是写推荐接口时从相似度表里取的是“相似度降序排列的前20个商品”再用LEFT JOIN把已购买的物品ID排除掉。目的是减少无关计算量。4.3 推荐结果接口读表返回和实时过滤用户的推荐接口是 /api/recommend/list参数只有 userId 和 limit。逻辑很简单先查该用户最近下单的商品列表然后取这些商品在物品相似度表中的兄弟商品按相似度倒序排列再过滤掉该用户已购买的物品取前limit个返回。因为相似度表已经提前算好这个接口的SQL执行时间在一毫秒级实测在千级商品量下完全没有压力。还有一个加分设计就是推荐结果会带一个 reason 字段例如“因为你还没有购买过XX品牌系统为你推荐了同类型商品”。这个字段能让用户感觉推荐系统不是玄学而是有理有据的。实现这个字段只需要在查相似度表时把 source_item_id 一同查出来前端展示时拼一句文案即可成本几乎为零但对用户体验的提升非常明显。5. 前后端分离的关键联调点接口规范、鉴权、状态管理前后端分离之后代码层面互相看不到双方唯一的契约就是接口文档。这块做得好不好直接决定两个人的协作效率。这个项目里我定了一套最小可行的规范非常实用。5.1 统一响应体与接口前缀后端所有的接口返回统一的 Result 结构{ code: 200, message: success, data: {} }code 非200时代表异常前端 axios 拦截器会统一弹出错误提示。接口路径统一以 /api 开头Controller 类上标注 RequestMapping(/api/manage) 或者 /api/user 前缀。这样做的好处是前端代理配置非常简单只需要代理一个前缀就够了。业务上所有需要登录的接口都加了一个自定义注解 RequireLogin通过 HandlerInterceptor 统一拦截校验Token。5.2 登录鉴权JWT 还是 Session这个项目用的是 JWT。原因很简单前后端分离后后端需要保持无状态Session 存储在服务器内存里多实例部署时就会出现“请求被另一台机器处理导致Session失效”的问题。JWT 把用户信息加密存放在客户端每次请求带上后端只负责验签和数据解析。实现时用户登录成功后生成一个有效期两小时的 Token前端存放在 localStorage 里axios 请求拦截器自动从 localStorage 读取并拼到 Authorization Header 上。这里要提醒一个安全问题JWT 存在 localStorage 会有XSS窃取的风险。更好的方案是放到 HttpOnly Cookie 里由后端自动携带。只是开发阶段为了方便调试我才选择了 localStorage。如果你要把系统用于生产环境建议改成 Cookie 方案这属于“上线前必须处理的隐患”级别的问题。5.3 Vue3 状态管理Pinia 的使用Vue3 官方推荐的状态管理库是 Pinia它比 Vuex 更简洁没有 mutations 的概念直接改 state 即可。这个项目里我把用户信息、购物车数量、权限标识放在 Pinia store 中统一管理。举个例子用户登录成功后后端返回用户基本信息和Token前端调用 store 的 login 方法把数据写入 state 的同时同步到 localStorage。页面刷新时Pinia 重新初始化根组件会再调用一个 validateToken 接口验证登录状态保证刷新后不掉登录。初学的时候最容易犯的习惯是把所有数据全放 Pinia其实组件内部的临时状态完全没必要进store过度使用反而会让状态流混乱调试困难。6. 实操部署从环境准备到前后端启动踩坑记录这部分讲怎么把这个项目真正跑起来。假设你手头是干净的Windows或Mac环境没有装任何Java开发工具。6.1 环境准备清单首先要装的是几个基础软件它们分别是 JDK 1.8、Maven 3.6、MySQL 5.7、Node.js 14。JDK 我建议用1.8版本对于老项目兼容性最好。MySQL 要注意字符集如果你用的是MySQL 8.0以上版本创建数据库时最好指定 utf8mb4否则中文数据有可能变成问号。创建数据库的命令如下CREATE DATABASE office_supplies DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;项目里附带了 init.sql 文件包含建表语句和测试数据直接在Navicat里执行即可。这里要说明一点测试数据不止包含20个商品还包含50个用户行为日志这些日志是推荐算法保证有效展示的核心素材。新手容易忽略造数的重要性空库跑起来之后“猜你喜欢”一大片空白然后开始怀疑算法写错了。第一次运行时一定要造足够的行为数据建议至少200条浏览、加购记录否则推荐结果跑不出来。6.2 后端启动三步走第一步修改 application.yml 里的数据源配置把用户名、密码换成自己本地的值。第二步在项目根目录执行 mvn spring-boot:run 启动后端服务。第三步等待日志里出现 “Started Application in xx seconds” 后用 postman 测试 http://localhost:8080/api/user/list 是否能返回JSON。后端启动常见的坑有两个。第一个是端口被占用日志会报 Port 8080 was already in use解决办法是换一个端口或者用 lsof -i:8080 找到占用进程干掉。第二个是 MyBatis XML 文件扫描不到控制台会报 Invalid bound statement (not found) 错误。出现这个错误不要慌先检查 mapper 接口和 XML 的 namespace 是否完全一致再检查 XML 文件是不是放在了 resources/mapper 目录下。Spring Boot 默认只扫描 classpath:mapper/*.xml放错目录会导致绑定失败。6.3 前端启动与代理配置前端项目是标准的 Vite 工程。在 terminal 里进入 frontend 目录依次执行 npm install 和 npm run dev。如果 npm install 很慢建议先执行 npm config set registry https://registry.npmmirror.com 把源切到国内镜像。启动成功后浏览器打开 http://localhost:5173 会进入系统登录页。开发环境的跨域问题靠 Vite 代理解决在 vite.config.js 里配置如下export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端发的所有 /api 请求都会被 Vite 开发服务器转发到后端 8080 端口浏览器端毫无跨域报错。6.4 模拟用户操作与推荐效果验证系统跑通之后推荐效果是肉眼可见的。用测试账号登录前台随便点开五六个商品详情页每个停留几秒再返回这时候首页的推荐位就会开始变化。如果我这篇文章之外你拿到了源码强烈建议按下面这个流程验证推荐算法首先用A账号只浏览“打印耗材”类别的三个商品然后用B账号浏览完全不同的“文具”类别接着让A账号再登录推荐位应该出现其他打印耗材商品而不是文具。如果符合这个预期说明协同过滤核心链路正常。有一个地方值得特意提醒第一次登录时推荐位是空的因为没有任何物品相似度缓存。别紧张这是预期行为。定时任务每10分钟跑一次跑完一次后推荐位就会有数据。如果你不想等10分钟也可以在页面里加一个调试按钮调一次 recompute 接口手动触发计算。7. 问题排查与避坑速查表实操过程中以下问题是我被问得最多的也是自己一路踩过来的坑整理成速查表供参考。现象可能原因解决办法前端请求接口404Vite代理未生效确认vite.config.js里server.proxy配置正确并重启前端接口返回500数据库密码错误或SQL语法错误查看后端控制台完整异常堆栈按行号定位登录后请求提示未授权Token未带上或过期检查localStorage是否存有token重新登录中文字段显示乱码MySQL连接字符集问题在jdbcUrl后追加 useUnicodetruecharacterEncodingutf-8推荐位长时间为空定时任务未触发或行为数据太少手动调用recompute接口检查日志表是否有行为记录同一商品重复推荐相似度表有脏数据清空 item_similarity 表重新计算一次接口响应慢未走索引的全表扫描给 operation_log 表加联合索引user_id, item_id, action_type操作日志表的索引是特别值得强调的点。这个表在推荐计算里会被频繁查询如果数据量到了几万行而没有索引计算一次的耗时会被放大到分钟级别。我加的联合索引解决了绝大部分性能问题查询直接从全表扫描变成了索引范围扫描耗时降了一个数量级。最后分享一条我在实际项目里的经验日常办公用品直售系统重点不在复杂功能而在基础链路的稳定性和数据的真实有效性。你花一周时间把推荐算法写出来不如花一周时间把用户行为日志的采集做扎实。算法再花哨喂进去的数据是脏的输出一定不可信。项目源码本身是一个很好的起点动手跑起来把推荐接口的返回结果打印出来你看看那20条推荐里有没有你刚刚加购的商品如果算法真的聪明系统应该把同类型消耗品排到最前面这才是办公用品推荐系统真正的价值所在。