资讯动态

SpringBoot+Vue前后端分离社区购物系统开发与部署全解析

发布时间:2026/10/14 6:12:04 来源:尧图企业网站定制
做这类社区购物系统的开发者大概率都体会过同一种尴尬项目源码拿到手后端能启动、前端也能编译可一登录就报错下单的时候购物车数据丢一半部署到服务器后前端页面能开但接口全是404。我这段时间正好把一个基于SpringBootVueMyBatisMySQL的前后端分离小区应急购物系统从零到一完整跑通了一遍从建库到上线踩了不少坑也把其中值钱的细节都沉淀了下来。这篇文章就围绕这套系统的完整源码和部署教程把技术选型、功能拆解、数据库设计、前后端联调以及生产部署这几个关键环节一次讲透。适合正在做毕业设计、接手类似社区采购项目、或者想用一套完整业务练手前后端分离技术的同学直接照着这篇文章就能复现出一套能用的系统。1. 这套系统到底在解决什么从真实需求到功能边界1.1 使用场景与角色分析先别急着谈技术把业务想清楚比什么都重要。这套系统的核心使用场景是特殊时期小区封闭管理下的物资采购居民不能出门但日常生鲜、药品、生活用品必须有人管。放在这个场景里参与的角色其实只有三个居民、社区管理人员、供应商或配送人员。居民需要什么打开系统看到还能买的商品加入购物车下单等通知取货或配送。社区管理人员需要什么维护商品信息、上下架商品、处理居民订单、安排配送、看到整体采购数据。供应商或配送人员在这个系统里通常不直接登录后台而是由管理员代为维护商品、查看订单配送列表。所以这个系统本质上不是复杂的电商平台而是一个“人为干预很强、订单密度不高、流程必须可靠”的社区采购工具。功能边界也因此很清晰要有用户注册登录、商品分类与展示、购物车、订单创建与状态流转、后台商品管理、订单管理、用户管理。支付功能在这个场景里往往不需要做死因为物资通常是在线下取货时结算或用社区统一记账。1.2 核心功能模块一览模块使用端核心功能用户认证两端共用注册、登录、退出普通用户和管理员角色区分商品模块用户端展示、管理端维护商品列表、分类筛选、商品详情、上下架、库存管理购物车用户端加购、改数量、删除、清空购物车数量角标订单模块用户端管理端下单、订单列表、订单详情、状态修改、取消订单配送管理管理端待配送列表、配送状态更新、取货通知数据统计管理端订单量、销售额、商品销量排行功能边界一定要控制住。我看过很多类似的代码坏就坏在给用户端塞了一堆社区公告、意见反馈、优惠券、满减活动结果核心下单链路反而做得稀烂。这套系统的价值在“可靠地完成交易闭环”不在“功能丰富”。1.3 业务上容易忽略的三个点第一库存不能为负。居民抢购时经常一次下单多件如果库存扣减不加控制超卖是必然的。第二订单状态必须单一职责。一个订单只能处于一个明确状态不能用字符串随意记。第三购物车不是必须跟服务端实时同步。在小并发场景下购物车数据存本地或存Session反而更简单可靠登录后用接口拉取就可以了。2. 技术选型为什么是这四件套配合逻辑与取舍2.1 每个组件在系统里的角色SpringBoot是后端地基负责把HTTP请求接进来、做业务处理、再返回结果。Vue是前端的面负责渲染页面、交互、调用接口。MyBatis是后端和数据库之间的桥把SQL写在Mapper里灵活可控。MySQL负责所有结构化数据的存储。这四者的关系可以这么理解Vue是餐厅的前厅SpringBoot是后厨MyBatis是传菜员MySQL是仓库。很多人会问为什么不直接用JSP那套传统开发模式前后端分离最大的好处是前端和后端可以独立开发、独立部署、独立扩展。前端跑在Nginx上后端是一个Java进程联调时只需要约定接口协议。对于这种社区购物系统团队里如果有人专攻前端、有人专攻后端分离模式能极大减少互相等待的时间。而且上线后如果只想优化页面不需要重新打包后端。2.2 为什么选MyBatis而不是MyBatis-Plus或JPAMyBatis的选择要分两个层面说。和MyBatis-Plus相比MyBatis-Plus确实开发效率更高但作为学习项目和毕设项目手写SQL能让你对SQL执行细节更敏感。尤其是订单汇总、商品销量统计这类带条件聚合的查询手写SQL会让你清楚知道索引怎么用、JOIN怎么走。和Spring Data JPA相比MyBatis把SQL显式写在XML或注解里出了问题方便直接拿到数据库里跑一遍排查对单机事务模型很简单的中小型系统来说足够直观。2.3 前端为什么是Vue而不是ReactVue的优势在于渐进式上手模板语法接近传统HTML思维中文资料也多Composition API用起来后逻辑复用也方便。如果你是一个人同时写前后端Vue的学习成本和维护成本会更友好。系统里的管理员后台不需要太复杂的组件树用Vue Router管理页面、Vuex或Pinia管理登录态Element UI库直接套表格和表单开发速度非常快。3. 业务主线拆解从浏览商品到确认收货的状态机3.1 用户侧完整流程用户访问系统后先注册账号管理员在后台把普通用户角色激活或者注册接口直接放行。登录成功后进入商品列表页按分类切换、把商品加入购物车。购物车里可以调整数量数量不能超过库存。提交订单时要填写或确认收货地址生成订单后返回订单详情。接下来就是等待管理员发货订单状态从“待配送”变成“配送中”最后变成“已完成”。整个链路最核心的就是下单这一步。后端接口需要同时做几件事校验商品是否在售、校验库存是否充足、计算订单总金额、扣减库存、生成订单主表记录、生成订单明细表记录、清空购物车中已下单的商品。这七件事必须在一个事务里完成任何一步失败都要整体回滚。3.2 订单状态机的设计我建议用枚举或常量定义状态值而不是在代码里到处写字符串。状态定义如下状态值含义说明0待确认下单成功等待管理员确认1待配送管理员已确认等待打包配送2配送中已安排配送或等待自提3已完成用户确认收货或管理员标记完成4已取消用户取消或管理员取消状态流转必须单向可控。不要允许用户把已完成的订单改回配送中也不允许取消已经配送到订单。管理端在修改订单状态时后端要做校验判断当前状态是否允许跳到目标状态。3.3 管理端核心操作管理端是这套系统真正产生管理价值的地方。商品管理包括新增商品、编辑价格和库存、上传商品图片、上下架。订单管理包括查看所有订单、按状态筛选、修改订单状态、取消订单。用户管理包括查看注册用户列表、启用或禁用账号。数据统计主要看今天订单数、总销售额、热销商品排行SQL用GROUP BY加SUM就能实现。4. 数据库设计表怎么拆字段怎么定4.1 核心表结构概览数据库设计我按“用户、商品、订单、购物车”四条线展开。通用字段如create_time、update_time每张表都加后面排查问题会省很多事。用户表user核心字段id、username、password、nickname、phone、address、role、status、create_time。role用1表示管理员、0表示普通用户。密码必须加密存储不要明文。商品分类表categoryid、name、sort、create_time。字段简单但分类影响前端筛选项必须保留。商品表productid、category_id、name、description、image、price、stock、sales、status、create_time、update_time。price用decimal(10,2)不要用float避免金额精度问题。image字段推荐存相对路径或URL前端直接拼接访问。购物车表cartid、user_id、product_id、quantity、create_time。不需要update_time。可以使用唯一索引user_idproduct_id防止同一个用户重复加同一商品时产生多条记录。订单主表ordersid、order_no、user_id、total_price、status、receiver_name、receiver_phone、receiver_address、remark、create_time、update_time。order_no要唯一可以用时间戳加随机数生成。订单明细表order_itemid、order_id、product_id、product_name、product_image、price、quantity、subtotal。这里刻意冗余了product_name和product_image因为商品信息可能后续被修改或删除但订单快照必须保存下单时的信息。4.2 外键到底建不建我的建议是逻辑外键不建物理外键。理由很实际物理外键会让插入和删除的顺序变得极其死板尤其在后端使用事务的时候一旦报错排查起来很难受。但逻辑外键索引必须建比如order_item表的order_id要建普通索引cart表的user_id和product_id要建联合唯一索引否则数据一多查询就很慢。4.3 一个容易被忽略的字段order_no订单号千万不能直接用自增id原因有两个。第一自增id会暴露系统订单量这不安全。第二用户和客服沟通时需要报一个简洁且唯一的单号。生成方式可以用yyyyMMddHHmmss加用户id后四位加随机三位数虽然不保证绝对唯一但在这种业务量级下冲突概率极低。如果要更严谨可以在数据库加唯一索引兜底。5. 后端关键实现SpringBootMyBatis代码这样组织才能少返工5.1 后端工程结构与分层后端包结构我建议严格分成controller、service、mapper、entity、common五层。controller只做参数接收和结果返回不写业务。service负责业务逻辑事务注解打在service层方法上。mapper是数据访问层接口加XML实现。common放统一返回结果类、异常处理类、工具类。com.example.shop ├── controller ├── service │ └── impl ├── mapper ├── entity ├── dto ├── vo └── common ├── Result.java ├── ResultCode.java └── GlobalExceptionHandler.java5.2 统一返回格式和全局异常处理前后端分离最忌讳接口返回格式不统一。前端拿到的数据结构一会儿是data直接返回数组一会儿是包了一层对象联调时非常崩溃。我通常定义一个Result类所有接口都返回这个结构public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }全局异常处理类用RestControllerAdvice捕获业务异常和系统异常避免把异常的堆栈信息直接暴露给前端。比如库存不足抛一个自定义BusException前端只需要根据code判断失败原因即可。5.3 登录和接口拦截登录接口校验用户名密码后生成一个token返回给前端。最经典的做法是用JWT把userId和role放进token里。前端请求时在Header里带Authorization字段后端写一个拦截器解析token、把用户信息放到ThreadLocal里。拦截器要做白名单配置比如登录、注册、商品列表这些接口放行购物车和下单相关接口必须登录。public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new BusException(未登录); } // 解析token校验有效性和过期时间 Long userId JwtUtil.getUserId(token); Integer role JwtUtil.getRole(token); UserContext.set(userId, role); return true; } }5.4 下单接口的事务控制下单接口是体现后端功底的核心方法。伪代码逻辑是先校验商品状态和库存然后扣减库存再创建订单主表和明细表最后删除购物车记录。整个过程必须加Transactional而且要注意异常被捕获后需要手动回滚。不要直接在controller里写这一大坨逻辑一定要放到service层。Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateDTO dto) { // 1. 校验用户地址和购物车 ListCart cartList cartMapper.selectByUserId(dto.getUserId()); if (cartList.isEmpty()) { throw new BusException(购物车为空); } // 2. 计算总价并校验库存 BigDecimal totalPrice BigDecimal.ZERO; for (Cart cart : cartList) { Product product productMapper.selectById(cart.getProductId()); if (product.getStatus() ! 1) { throw new BusException(商品已下架); } if (product.getStock() cart.getQuantity()) { throw new BusException(商品库存不足); } totalPrice totalPrice.add(product.getPrice().multiply(new BigDecimal(cart.getQuantity()))); } // 3. 创建订单 Orders order new Orders(); // 设置订单号、用户、金额、地址等 orderMapper.insert(order); // 4. 插入订单明细并扣库存 for (Cart cart : cartList) { orderItemMapper.insert(...); productMapper.decreaseStock(cart.getProductId(), cart.getQuantity()); } // 5. 清空购物车 cartMapper.deleteByUserId(dto.getUserId()); return orderVO; }这里为了安全库存扣减SQL要写成条件更新UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}如果影响行数为0说明库存已经被其他请求扣完了直接抛异常回滚。这种方式比先select再update靠得住得多。5.5 MyBatis动态SQL的典型用法商品列表查询往往需要根据分类、关键字、状态动态拼接条件这种场景用MyBatis动态SQL很顺手select idselectProductList resultTypecom.example.shop.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 teststatus ! null AND status #{status} /if /where ORDER BY create_time DESC /select注意LIKE语句不要直接拼字符串用CONCAT拼接参数能避免SQL注入。MyBatis的#{}会预编译参数但${}是直接拼接页面搜索框这种入口一定只用#{}。6. 前端Vue对接细节角色路由与状态同步6.1 前端工程结构Vue这边我用的是Vue CLI创建的标准工程配合Vue Router和Axios。页面放在views下组件放components接口请求统一放在api目录下。路由按角色拆分用户端页面有首页商品列表、购物车、订单列表、订单详情、个人中心管理端页面有商品管理、订单管理、用户管理、数据统计。src ├── api │ ├── product.js │ ├── cart.js │ ├── order.js │ └── user.js ├── router │ └── index.js ├── store │ ├── index.js │ └── user.js ├── views │ ├── home │ ├── cart │ ├── order │ ├── user │ └── admin ├── utils │ └── request.js └── App.vue6.2 Axios封装和登录态保持前端必须统一封装请求实例否则每个页面都要写一遍baseURL和token简直灾难。我在utils/request.js里创建axios实例设置baseURL为/api加上请求拦截器和响应拦截器。请求拦截器里从localStorage取出token挂到Header上。响应拦截器里统一处理code如果code为401就跳转登录页。const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use(res { const code res.data.code if (code 200) { return res.data } if (code 401) { router.push(/login) } return Promise.reject(new Error(res.data.message)) }, error { return Promise.reject(error) })登录成功后把token和用户信息存到localStorage和Vuex中刷新页面时再从localStorage恢复用户登录态。整个过程不用后端参与简单可靠。6.3 路由守卫和角色控制路由守卫在每次路由跳转前判断登录状态。如果去的是需要登录的页面而token不存在就强制跳转登录页。管理员页面的判断逻辑更严格除了需要登录还要检查当前用户role是否为1否则跳回首页。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else if (to.path.startsWith(/admin) !isAdmin()) { next(/) } else { next() } })6.4 购物车数据同步的前端处理购物车页面从后端拉取购物车列表展示每个商品的数量、单价和小计。修改数量时可以调后端接口更新quantity也可以等点击“去结算”时统一把购物车数据提交上去。我推荐前者每次改数量就调接口后端同步校验库存。加购成功后刷新一下购物车数量角标这个值可以从后端返回的cartCount里取。下单成功后要做的第一件事是清空前端购物车视图同时更新库存展示否则用户会看到已经买完的商品还在购物车里体验极差。7. 从源码到能访问本地运行和服务器部署的完整步骤7.1 环境准备和版本选择先明确版本这些是我实测跑通的组合组件版本说明JDK1.8 或 11SpringBoot 2.x均支持Maven3.6后端依赖管理Node.js14 或 16Vue CLI项目构建MySQL5.7 或 8.0均可Nginx1.20生产环境静态托管和反向代理JDK不要用太高版本SpringBoot 2.7配JDK17在某些环境下会出反射告警没必要折腾。MySQL如果本地是8.0版本驱动名和时区配置需要多注意后面统一说。7.2 初始化数据库拿到源码后先在数据库里执行项目提供的sql脚本。执行时注意编码最好用UTF-8。如果sql文件里没有字符集设置建表后手动确认表编码是utf8mb4否则中文字段容易乱码。mysql -uroot -p shop.sql执行完成后进入数据库查看表是否创建成功。重点检查orders、order_item、product这几张核心表。如果表已经存在需要先drop再导入否则重复执行会报错。7.3 修改后端配置并启动后端配置文件application.yml最关键的就是数据源配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/shop?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case必须为true否则数据库字段下划线无法映射到实体类的驼峰属性查询结果全是null。启动后端mvn spring-boot:run看到日志输出“Started Application”且Tomcat started on port 8080就说明后端起来了。先用接口测试工具调一下商品列表接口确认能返回JSON数据再做前端联调。7.4 前端本地开发环境启动前端源码目录执行npm install npm run serve默认端口是8080但后端已经占了8080端口所以Vue CLI一般会自动把前端端口跳到8081或让你选择其他端口。这里就会出现跨域问题需要在前端项目根目录新建vue.config.js配置代理module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: /api } } } } }这样前端开发环境下请求/api/product/list就会代理到后端的8080端口接口能通存储在本地的登录态也不会出问题。7.5 生产部署jar包加Nginx本地跑通之后部署到服务器是另一套流程。后端打包mvn clean package -DskipTeststarget目录下会生成一个可执行的jar包。上传到服务器后直接运行nohup java -jar shop.jar --spring.profiles.activeprod shop.log 21 可以通过日志文件实时查看是否启动成功。后端进程放后台跑避免SSH断开后进程被杀。前端打包npm run build生成dist目录把dist目录上传到服务器的Nginx静态目录。Nginx配置需要做两件事第一是把根路径指向dist目录第二是把/api路径反向代理到后端8080端口。关键配置如下server { listen 80; server_name 你的服务器IP或域名; location / { root /usr/share/nginx/html/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files那行必须写否则Vue Router用history模式时刷新非首页路径会报404。配置完成后执行nginx -t校验语法再重载Nginx。8. 前后端联调与部署阶段最容易踩的坑8.1 跨域配置的两种写法本地代理方式和Nginx反向代理方式解决的是同一个问题。如果你在axios封装里写死了baseURL为http://localhost:8080那么部署到线上后所有请求都会打到开发环境的地址这在生产环境里属于硬编码事故。更好的做法是封装里只用相对路径/api本地开发代理到后端生产环境由Nginx把/api转发到后端jar包。这样代码不用动一套环境切换成本最低。8.2 token在Header里丢失很多项目前端明明存储了token但后端拦截器一直报未登录。原因通常是axios请求拦截器里读token的key和后端写入的key不一致或者后端的JWT过滤器没有从Header里读到Authorization。建议前后端约定一个统一的Header名并且在浏览器开发者工具的Network面板里直接看发出去的请求带没带这个Header问题一下就能定位。8.3 MySQL版本带来的时区问题MySQL 8.0和SpringBoot 2.x组合时连接串必须带serverTimezoneAsia/Shanghai否则会报“Cannot resolve time zone”异常。另外MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver老项目里写的是com.mysql.jdbc.Driver虽然能兼容但会有警告建议改成新版驱动名。8.4 图片上传后访问不到商品图片是电商系统绕不开的功能。如果项目把图片存到本地磁盘部署时绝对路径要配置正确。前端访问图片的URL和Nginx的配置需要联动。最省事的方案是配置一个location /images/对应到磁盘上的上传目录location /images/ { alias /home/shop/upload/; }如果上传后只能看到文字描述、看不到图99%是图片访问路径没走对一个可访问的资源前缀。8.5 前后端字段名不一致导致数据空白后端返回给前端的JSON字段是驼峰命名的前端页面里写的字段如果差一个字母就会导致列表数据空白但接口正常返回。排查思路很简单直接看浏览器Network里接口的response数据看字段名是否和页面代码里绑定的名称一致。后端实体类可以通过JsonProperty或统一VO对象来控制输出字段名称我建议后端不要直接裸返回entity而是返回VO对象这样不但能隐藏敏感字段还能控制字段名。8.6 端口和防火墙问题服务器部署后外网访问不到先检查Nginx是否启动再检查服务器安全组是否放行80端口。后端8080端口不需要对外开放内部通过Nginx反代访问就够了。这个细节能减少很多安全风险。我这段时间把整套系统过了一遍最大的感受是这类社区购物系统的业务链路并不复杂但每一步都要做到“状态可控、字段可查、接口可测”。数据库表结构提前设计好后端事务边界划分清楚前端接口用封装统一管理部署时Nginx配置仔细核对整个项目下来基本不会出大问题。如果你也在折腾这套源码建议先本地把核心交易流程跑通再考虑扩展后台统计和图片上传这些附加功能。那套JWT拦截器加订单状态机的设计思路以后换任何业务系统都能直接用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑