SpringBoot+Vue办公用品直售系统:完整前后端分离实战
简介一份基于JAVASpringBootVueMySQL的日常办公用品直售系统完整项目面向高校学生及开发者适合作为毕业设计、课程设计或期末大作业参考也可作为前后端分离开发的学习范例。包内整合了项目源码、数据库脚本、开发工具及构建运行脚本共597个文件主要包含Java后端逻辑、Vue前端组件、JavaScript交互、SVG图标、CSS样式、JPG界面图及SQL数据脚本等整体压缩包仅16.1MB结构清晰便于检索。系统覆盖商品展示、购物车管理、订单处理、用户管理、支付结算等核心模块界面简洁美观后台管理便捷项目已严格调试并通过导师指导下载后可直接运行使用。目前已有39人学习下载适合需要完整可运行项目作为设计参考或希望快速了解SpringBoot与Vue整合开发的读者。1. 一个能用 SpringBoot 和 Vue 完整跑通的办公用品直售系统办公用品采购一直是个“看起来简单、做起来琐碎”的事情线下比价、填单、审批、领用每一步都在消耗行政和员工的时间。网上买个笔记本、签字笔、打印纸和买衣服买电器逻辑完全不一样它需要“直售”而不是“平台撮合”需要把商品、库存、订单、支付、售后这些环节做成一条能直接交付给企业或学校内部使用的闭环。这个基于 JAVA SpringBoot Vue MySQL 的日常办公用品直售系统正是冲着这个场景去的——它不是一个只演示增删改查的毕设壳子而是一套可以真正注册登录、浏览商品、加购结算、生成订单、管理库存、处理用户权限的完整前后端分离应用。拿到这份源码时第一感觉是“工程味很正”项目里有 build.bat、run.bat、3-build.bat、2-run.bat 这类一键脚本也有 update-password.vue.bak、main.js.bak 这样的备份文件——说明开发者在调试过程中保存过关键节点。技术栈非常主流后端 SpringBoot 做接口服务前端 Vue 2.x 做单页应用MySQL 存数据Maven 管依赖IDEA 开发Navicat 导库。适合谁想拿真实项目做毕设参考的高校学生或者公司内部需要一个轻量办公用品采购后台的开发者。跑通它不需要魔改导入数据库、启动后端、启动前端三步就能看到完整的购买流程。2. 系统定位与技术选型为什么是 SpringBoot Vue而不是别的2.1 直售模式与电商模式的核心差异做电商项目的同学很多但大部分毕设都是“模拟淘宝”式的通用商城而直售系统Direct Sales System强调的是厂家到消费者的单级链路没有商家入驻、没有多店铺、没有平台抽佣逻辑。这意味着系统里的权限模型、商品模型、订单模型都更简单也更适合做深入的功能打磨。这套系统里可以看到一个关键设计用户端和管理端共用同一套后端接口靠 Spring Security 或拦截器做角色区分。前端页面虽然只有一个 Vue 工程但通过路由守卫和菜单权限控制了“普通用户看到的是商城”、“管理员看到的是后台管理面板”两个截然不同的工作台。这种设计比单独拆一个 admin 前端更贴近中小型企业的真实做法——成本低维护方便。2.2 技术栈选择的实际理由SpringBoot 2.x内嵌 Tomcat打 jar 直接跑省去单独部署 Web 服务器的麻烦。对于毕设演示和中小型项目这个“开箱即用”特性比 SSM 时代省了大量配置时间。Vue 2.x Element UIVue 的响应式数据绑定让“购物车数量变化、订单金额重算”这种交互很自然Element UI 自带表格、表单、弹窗、分页组件后台管理页的开发效率极高。MySQL 5.7事务支持稳定InnoDB 引擎处理订单这种强一致性数据可靠Navicat 可视化操作对新手很友好。Maven依赖版本统一管理pom.xml里加个 dependency 就能引入 MyBatis、Druid 连接池、Lombok 等避免手动导 jar 的混乱。这套组合在 2024 年的今天看起来不算新潮但它恰好覆盖了一个核心需求好部署、好调试、好讲清楚。新人对 Spring Boot 的学习成本低导师评审时也容易看懂架构二次开发时有大量现成的社区资料可查这些都是硬选型时不能忽略的优势。2.3 三层结构Controller、Service、Mapper 的职责划分后端代码的包结构是典型的教科书式分层但分层里藏了实际项目的细节。controller层只做参数接收和结果封装service层处理业务逻辑比如下单时库存扣减和订单生成的原子性mapper层用 MyBatis 写 SQL 操作数据库。前端api目录单独封装 axios 请求每个接口都有独立的请求函数页面组件里不直接出现裸的请求地址。这套结构的好处是后期想换数据库、想加 Redis 缓存、想接消息队列影响范围只限于 service 层。我之前拆过一个不分层的毕设所有逻辑堆在 Controller 里改一个订单状态要翻几百行代码对比之下这个项目的代码组织是可以在评审时拿出来讲“工程性”的。2.4 环境版本与开发工具清单项目对版本的要求不苛刻但也存在几个隐性约束。JDK 1.8 是主流选择Maven 3.6 基本不会出问题MySQL 建议 5.7 而不是 8.0——虽然 8.0 也能跑但数据库脚本里如果有ENGINEInnoDB和DEFAULT CHARSETutf8mb4在 8.0 下偶发时区报错5.7 反而更稳。IDE 用 IDEA原因很实在Lombok 插件支持最好、Spring Boot 的 Run Dashboard 直观、内置的 HTTP Client 可以快速测试接口。前端开发建议安装 VSCode 的Vetur插件.vue文件的语法高亮和格式化会正常很多。部署环境上前端构建后生成的dist目录可以直接扔到 Nginx 或 Tomcat 的静态目录里也可以用npm run serve开开发服务器通过 Vite 或 Webpack 的 proxy 配置代理/api请求到后端 8080 端口解决跨域。3. 核心模块拆解一个商品从浏览到订单支付要经过哪些代码3.1 商品展示模块动态数据渲染与图片处理商品展示是用户进入系统看到的第一屏。前端IndexMain.vue负责中央内容区渲染IndexAsideStatic.vue是侧边栏分类导航BreadCrumbs.vue处理页面路径提示。商品列表不是静态写死的 HTML而是通过 axios 从后端请求/product/list接口拿动态数据。后端对应的 Controller 代码大致如下RestController RequestMapping(/product) public class ProductController { Autowired private ProductService productService; GetMapping(/list) public Result getProductList(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String category, RequestParam(required false) String keyword) { PageInfoProduct pageInfo productService.queryList(pageNum, pageSize, category, keyword); return Result.success(pageInfo); } }pageNum是当前页码pageSize是每页条数category可选地传入办公用品分类keyword支持按商品名称模糊搜索。这里最实用的细节是PageInfo——它是 MyBatis 的分页插件 PageHelper 的返回对象封装了总条数、总页数、当前页数据列表前端拿到后可以直接驱动 Element UI 的el-pagination组件不需要手写 count 查询。商品图片的处理方式是相对路径存储数据库product表里img字段存的是uploads/xxx.jpg这样的字符串前端会用完整的访问路径拼接展示。开发环境里后端通过静态资源配置将本地磁盘的uploads目录映射为 URL 路径spring: resources: static-locations: classpath:/static/, file:${UPLOAD_DIR:./uploads/}UPLOAD_DIR是环境变量默认是项目根目录下的uploads文件夹。这个写法很值得抄——把图片上传目录暴露为可配置项将来部署到云服务器时可以直接指定 OSS 挂载路径或用nginx映射不用改 Java 代码。3.2 购物车管理用户会话与临时数据持久化的选择购物车的实现方式在毕设里常见两种存 Session 或存数据库表。这个项目用的是数据库表方案。Session 方案有一个致命缺陷——用户换设备或清除浏览器缓存购物车就空了而且后台无法统计“哪些商品被加入购物车但未结算”。数据库表方案虽然多一次 IO但可以获得用户维度的购物车快照支持跨端同步。购物车的核心数据表字段包括用户ID、商品ID、购买数量、加入时间。注意它没有冗余商品价格和商品名称因为商品信息随时可能被管理员调价或改名如果购物车里存了价格副本结算时就会出现“页面价格和订单价格不一致”的问题。查询购物车列表时通过 INNER JOIN 关联商品表获取最新信息SELECT c.id, c.product_id, c.quantity, c.create_time, p.name, p.price, p.img, p.stock FROM cart c INNER JOIN product p ON c.product_id p.id WHERE c.user_id #{userId} ORDER BY c.create_time DESC加入购物车的接口在CartController里映射为/cart/add前端点击商品卡片上的“加入购物车”按钮时会检查用户是否已登录。未登录的情况在路由守卫中被拦截跳转到登录页——这个业务规则虽然在很多项目里被做成前端判断但面试时审阅代码的人会重点看后端有没有做同样的校验。安全边界永远是后端的责任前端拦截只是体验优化。3.3 订单生成与状态机库存、金额、编号的原子化处理用户从购物车点击结算时前端会把选中的购物车记录 ID 列表提交到/order/create后端要在一个数据库事务里完成三件事校验商品库存、扣减库存、生成订单记录。这三步任何一步失败都不能留下脏数据。Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateDTO dto, Long userId) { // 1. 根据用户ID查询购物车选中项 ListCartItem cartItems cartMapper.selectCheckedItems(userId, dto.getCartIds()); if (cartItems.isEmpty()) { throw new BizException(购物车为空无法结算); } // 2. 计算订单总金额 BigDecimal totalAmount BigDecimal.ZERO; for (CartItem item : cartItems) { Product product productMapper.selectById(item.getProductId()); if (product.getStock() item.getQuantity()) { throw new BizException(product.getName() 库存不足); } totalAmount totalAmount.add(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 3. 生成订单号 String orderNo OFFICE System.currentTimeMillis() String.format(%04d, new Random().nextInt(10000)); // 4. 扣库存 for (CartItem item : cartItems) { productMapper.deductStock(item.getProductId(), item.getQuantity()); } // 5. 插入订单主表和订单明细表 Order order new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setTotalAmount(totalAmount); order.setStatus(0); // 0待支付 1已支付 2已发货 3已完成 4已取消 orderMapper.insert(order); for (CartItem item : cartItems) { OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(item.getProductId()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); } // 6. 清空已购买的购物车记录 cartMapper.deleteByIds(dto.getCartIds()); return orderVO; }Transactional注解保证了 4、5、6 步的原子性——任何一步抛异常都会回滚全部操作不会出现“库存扣了但订单没生成”的事故。订单号的前缀OFFICE加上时间戳和随机四位数的组合虽然简单但对于日单量几千的小系统完全够用而且肉眼一看就知道是办公用品平台的订单。订单状态机是项目中非常“毕设答辩加分”的设计点。状态字段使用 Integer 枚举值0 到 4 分别代表待支付、已支付、已发货、已完成、已取消。用户端可以取消待支付订单管理员可以修改订单状态为发货和完成。前端IndexHeader.vue的购物车图标上有角标数量提示这个角标的数据来源于CartController的/cart/count接口它返回一个int前端组件在mounted钩子中请求一次加购成功后通过全局事件刷新。3.4 支付结算模块第三方支付对接的“替代方案”真实的支付系统微信支付、支付宝支付需要商户号、证书、回调域名毕设环境里没有这些条件。这个项目的做法是在前端模拟支付流程——点击“去支付”按钮后前端弹出确认弹窗调用/order/pay接口后端将这个订单的状态从 0待支付置为 1已支付同时把pay_time字段写入当前时间。PutMapping(/pay) public Result payOrder(RequestParam Long orderId, RequestParam Long userId) { Order order orderMapper.selectByIdAndUserId(orderId, userId); if (order null) { throw new BizException(订单不存在); } if (order.getStatus() ! 0) { throw new BizException(订单状态异常无法支付); } order.setStatus(1); order.setPayTime(new Date()); orderMapper.updateById(order); return Result.success(null); }这段代码的精髓在于status ! 0的校验——防止重复支付。如果不加这个条件用户疯狂点击支付按钮订单的支付时间会被不断刷新看起来没问题实际上已经破坏了业务一致性。这个校验在真实项目中对应的是“幂等性”概念毕设代码里能出现这个意识评分时很加分。如果你想在这个基础上接支付宝沙箱环境改造点很清晰后端新增一个prepay接口返回支付宝支付的表单字符串前端打开新窗口提交然后配置一个/notify回调接口接收支付宝的异步通知在回调里校验签名、更新订单状态。核心的订单状态字段和支付时间字段已经设计好了不需要动表结构。4. 数据库设计九张核心表之间的关联关系4.1 数据库文件与初始化项目提供的 SQL 脚本导入 Navicat 就能完成建库建表。数据库名建议和脚本里的库名保持一致否则后端application.yml里的jdbc:mysql://localhost:3306/office_supply需要同步修改。导入的方式是在 Navicat 里新建查询把.sql文件内容直接粘贴执行或者用“运行 SQL 文件”功能。执行时要注意utf8mb4字符集——如果表已经是 utf8 但字段需要存 Emoji比如商品描述里放了表情符号会报字符集错误。4.2 用户与权限单表多角色还是拆表用户表是系统的基础。现实中电商网站会有user和admin两张表但这个小系统采用的是单表加role字段的思路——role为0表示普通用户为1表示管理员。好处是登录接口只需要查一张表通过 role 字段决定跳转的前端路由坏处是如果将来要扩展多种角色字段约束不够灵活。表结构关键字段id主键、username用户名唯一、password密码MD5 加密存储、nickname昵称、avatar头像、phone手机号、email邮箱、role角色标识。有一个容易踩坑的地方密码加盐。如果直接用 MD5 加密明文密码虽然看不出来原文但之前别人已经用彩虹表反向匹配过常见密码123456、admin888 等一查就破。我一般会在下单前把源码里DigestUtils.md5DigestAsHex改造成MD5(password 随机盐值)或者直接用 Spring Security 的BCryptPasswordEncoder这样安全性才能拉满。4.3 商品与分类一对多关系的表达商品表product是核心业务表字段包括id、name、category_id关联分类表、priceDecimal 类型精确到分、original_price用于展示划线价、stock库存、sales销量、img商品图片、description详细介绍、status上架状态。分类表category的字段只有id和name两个——因为办公用品的类目深度一层就够了不需要递归的无限级分类。为什么强调Decimal而不是Double这是一个经典的踩坑点。Java 的 Double 在计算金额时会有精度丢失比如0.1 0.2得到0.30000000000000004订单金额一旦涉及多商品累加误差就不可接受了。Decimal在 MySQL 里是定点数按字符串存储避免浮点误差在 Java 侧用BigDecimal做运算两边配合才是正确姿势。4.4 订单与购物车主从表结构订单表存储订单头信息order_no订单编号、user_id下单人、total_amount总金额、status状态、consignee收货人、phone、address、create_time、pay_time。订单明细表存储每个商品的快照order_id外键、product_id、product_name、product_price、quantity——这里必须冗余商品名称和价格的快照因为订单生成后商品可能被改名或调价历史订单必须展示当时的信息。购物车表的设计取舍有的方案里会在购物车表添加checked字段表示是否选中但这个项目用“前端 selected 状态 后端接收 cartIds 列表”的方式。前者的好处是后端能记忆用户的选择状态刷新页面不丢失后者胜在实现简单提交逻辑更直接。实际使用中购物车的数据量不会很大两种方案差别不明显但如果让我自己做我会加checked字段因为用户勾选状态丢失的体验问题在真实使用中经常被吐槽。5. 从零到一跑起来环境准备、启动步骤与避坑记录5.1 环境准备JDK、Maven、MySQL、Node.js 缺一不可跑这个项目之前先确认本地环境满足以下版本要求工具版本要求用途JDK1.8编译运行 SpringBoot 后端Maven3.6依赖管理与项目打包MySQL5.7存储业务数据Navicat任意版本导入 SQL 脚本、可视化查看数据Node.js10.x运行 Vue 前端开发服务器按这套列表检查环境时最痛苦的是 JDK 版本。先跑java -version看当前版本如果已经是 17 或 21建议安装一个 1.8 并存版本并在 IDEA 的 Project Structure 和 Settings Maven Runner 里都指定 JRE 为 1.8。版本不一致通常会在 Maven 编译时报Invalid target release或package javax.annotation does not exist前者是编译目标版本问题后者是 javax 包被移除的问题——所以老老实实按 1.8 配别硬刚高版本。5.2 启动四步走导库、开后端、开前端、验收第一步导入数据库。打开 Navicat新建连接右键“新建数据库”数据库名输入脚本里的名称字符集选utf8mb4然后右键运行 SQL 文件选择项目里sql目录下的.sql脚本。导入完成后展开表列表如果看到user、product、category、cart、orders、order_item这些前缀的表并且有数据行存在说明数据库就位。# 后端启动项目根目录执行 mvn spring-boot:runMaven 第一次执行时会下载大量依赖包这个过程根据网络状况可能需要几分钟到十几分钟。spring-boot:run会把 SpringBoot 应用启动在 8080 端口。日志里出现Tomcat started on port(s): 8080就是成功了。偶尔会有端口占用的情况执行netstat -ano | findstr 8080查到 PID 后杀掉或者在后端配置里改server.port。# 前端启动进入 vue 前端目录执行 npm install npm run servenpm install安装node_modules依赖会让你等一会儿。如果网络慢或镜像源超时设置一下国内镜像源。npm run serve启动开发服务器后浏览器访问http://localhost:8081具体端口以控制台输出为准很多 Vue 项目配了 8080 冲突时自动 1。前端通过代理把/api请求转发到后端 8080日常开发完全够用不需要额外配 Nginx。5.3 避坑指南我在启动和调试过程中遇到的几个实际问题现象一后端启动时报Access denied for user rootlocalhost原因数据库账号密码和后端配置文件里的不一致。 解决检查application.yml里的username和password改成自己本机 MySQL 的用户名密码。如果你用 Navicat 能连上数据库那说明账号密码没错可能是字符编码导致密码识别错误——直接复制 Navicat 的连接配置到 yml 里最稳。现象二前端页面能打开但所有列表都是空数据原因浏览器控制台大概率报proxy error说明前端的代理配置和后端端口不匹配。 解决看前端vue.config.js里proxy的target指向的是不是http://localhost:8080确认后端确切端口后修改。常见错误是后端改了server.port但没同步改前端代理。现象三导入 SQL 时某张表报字段长度错误或语法错误原因可能是 MySQL 版本差异。5.7 的 SQL 脚本在 8.0 的严格模式ONLY_FULL_GROUP_BY下反而更容易报错而 8.0 的脚本有些 5.7 不兼容的字符集定义。 解决优先匹配项目文档里说明的 MySQL 5.7 版本。如果只有 8.0把 SQL 文件里的utf8mb4_0900_ai_ci替换成utf8mb4_general_ci再执行。现象四前端 npm run serve 报Module build failed原因通常是 Node 版本过高Webpack 4 和 Node 17 的 OpenSSL 3.0 有兼容性问题。 解决执行NODE_OPTIONS--openssl-legacy-provider npm run serveWindows 用set NODE_OPTIONS--openssl-legacy-provider或者把 Node 降到 16 版本。这是 Vue 2 工程目前最常见的启动翻车现场。现象五登录接口返回成功但前端拿不到用户信息原因跨域情况下没有配置 Cookie 传输。 解决后端的跨域配置里要加上allowCredentials(true)前端的 axios 要设置withCredentials: true。之前在某项目里 debug 了一下午就是因为这个配置接口调通了但用户会话一直建立不起来。6. 二次开发指南把这个商改成带评论和优惠券的完整商城如果你想基于这套代码做更深入的课题或者交一个更有分量的课程设计我建议从三个方向入手每个方向都能在边学边改中踩出实战经验。第一个改造点给商品加评价功能。这是所有电商平台的标配但很多毕设都没有。表结构上新增一个product_comment表字段包括id、product_id、user_id、content、images逗号分隔的图片路径、rating1-5 评分、create_time。后端新增评论查询接口和发表接口查询接口按商品 ID 分页。前端在商品详情页 Tab 栏加一个“评价”标签已完成订单的商品旁边才会显示“评价”按钮——这个逻辑其实把订单状态机的 1已支付和 3已完成串起来了。在我改过的毕设里这个功能是最容易出彩的因为它逼迫你去理解“状态流转驱动用户行为”的业务设计。第二个改造点优惠券系统。优惠券看似简单做的时候才发现细节很多用户在优惠券中心领取、每种券有数量限制、下单时可用券且需要校验订单金额是否达到使用门槛、订单生成后券核销为已使用、订单取消后退券。表需要coupon、user_coupon两张表。下单流程里加入优惠券 ID 参数金额计算变为“商品总价 - 优惠券面额 实付金额”。注意一个坑如果订单部分退款券是否退回这个规则在不同平台都不一样你可以自己定义但必须在代码注释里写明逻辑方便评审时讲清楚。第三个改造点接入支付宝沙箱环境。前文提过这是最接近真实生产的改造。支付宝开放平台申请沙箱应用后后端引入alipay-sdk-java依赖配置应用的 APPID、网关地址、应用私钥和支付宝公钥。前端参数用异步通知验签的方式处理回调。这段改造如果在毕设答辩现场演示因为写的是真实沙箱代码在导师眼里分量和其他模拟代码完全不同。做完这些改造后验证方式不要只停留在手动点击。建议给订单管理接口写一轮 JUnit 集成测试用SpringBootTest注解直接跑真实环境测试用例覆盖“生成订单时库存不足抛异常”、“重复支付返回失败”、“取消订单后库存回滚”这三个核心场景。我用mockMvc模拟请求时发现能提前揪出 90% 的逻辑 bug尤其是事务回滚类的问题比手工测试高效太多。从那以后我每次拿到别人的毕设源码都会强制自己先搭一套能跑的测试骨架再动手看业务代码——不为别的只因为“能编译”和“跑得对”是两码事手动点一遍永远不如自动化用例来得踏实。这份办公用品直售系统代码结构干净、注释清晰花一个周末通读 Controller 和 Service 层再动手改两个你想要的功能这个过程本身就是最好的学习素材。希望这份拆解能帮你更快地把项目跑起来少走我踩过的那些弯路。本文还有配套的精品资源点击获取