
做了几个月的社区团购项目从零开始完成了一个基于SpringBoot和Vue的前后端分离系统整个项目涉及用户端、管理员端、商品管理、订单流程、部署上线等多个环节踩了不少坑也沉淀了一套完整的源码和部署方案。这篇文章把整个项目的设计思路、核心实现、联调要点和部署流程全部拆开讲清楚想用这个项目练手前后端分离实战的或者准备做毕业设计、简历项目的可以直接参考。这个项目选择的技术栈是SpringBoot Vue MyBatis MySQL是目前中小型前后端分离项目非常经典的一套组合也是Java后端和前端开发岗位面试里最常问到的技术栈组合。项目本身围绕社区团购业务展开包含用户浏览商品、下单、支付模拟、订单管理、后台商品发布与分类管理、订单配送状态流转等完整业务闭环覆盖了前后端分离项目开发中最常见、最核心的技术点RESTful API设计、Token鉴权、统一响应封装、动态SQL、跨域处理、打包部署等。适合有一定Java基础、想系统性提升前后端分离项目经验的开发者来跟做。1. 项目整体设计与业务场景拆解1.1 社区团购系统的真实业务链路社区团购和普通电商系统最大的区别在于“团长”这个角色和“自提点”的概念。用户通过平台下单商品不是普通快递发货而是统一配送到社区自提点用户到点自提。这个业务模型决定了系统在功能设计上不需要复杂的物流跟踪和运费模板更多是把精力放在区域化的商品管理、开团时间、库存扣减、提货码校验这些环节上。整个系统拆成用户端和管理员端两块。用户端C端提供的功能包括注册登录、浏览商品分类、查看商品详情、加入购物车、提交订单、在线支付模拟、查看个人订单、确认收货。管理员端B端提供的功能包括商品分类管理、商品上下架、库存维护、订单查看与发货、统计概览。1.2 功能模块划分与角色权限设计系统的核心角色只有两个普通用户和管理员。但在实现时我并没有把权限判断做得很粗放而是通过JWT Token中携带的用户角色字段配合后端的自定义注解和拦截器做鉴权。// 角色枚举 public enum RoleEnum { USER(0, 普通用户), ADMIN(1, 管理员); }管理员接口统一在Controller层通过RequireAdmin注解标识后端拦截器会解析Token从Redis中取出用户角色当角色不匹配时直接返回403。这种设计比在业务代码里到处写if (isAdmin) ... else ...要干净得多前后端配合时也能根据返回的状态码统一跳转和处理。功能模块的具体拆分如下表所示模块用户端功能管理员端功能用户模块注册、登录、个人信息用户列表、禁用/启用用户分类模块按分类浏览商品新增分类、编辑分类、删除分类商品模块商品列表、详情搜索新增商品、编辑商品、上下架、库存管理购物车模块加入购物车、修改数量、删除、批量结算-订单模块提交订单、取消订单、确认收货订单查询、订单发货、配送状态管理支付模块模拟支付流程对账查询1.3 项目目录结构与代码组织方式后端采用标准的Maven多级目录结构按照controller - service - mapper - entity分层的模式组织。我特意把config、interceptor、common、exception这些基础设施目录放到了业务包之外这样整个项目看起来更清晰也方便以后扩展其他业务模块。前端使用的是Vue 2 Vue Router Vuex Axios Element UI这个技术组合好处是生态成熟遇到问题能搜到大量现成方案。Vue 3 Vite 虽然更现代但对于这个项目里用到的功能和大多数开发者的熟练度来说Vue 2 Webpack 反而更稳妥。community-group-buying ├── backend (SpringBoot 2.7.x) │ ├── src/main/java/com/community │ │ ├── common // 统一响应、常量、异常 │ │ ├── config // 跨域配置、WebMvc配置 │ │ ├── controller // 接口层 │ │ ├── entity // 数据实体 │ │ ├── interceptor // JWT拦截器 │ │ ├── mapper // MyBatis接口 │ │ ├── service // 业务层 │ │ └── utils // JWT、日期等工具类 │ └── src/main/resources │ ├── mapper // MyBatis XML文件 │ └── application.yml └── frontend (Vue 2 Element UI) ├── src │ ├── api // 接口请求封装 │ ├── router // 路由配置与守卫 │ ├── store // Vuex状态管理 │ ├── views // 页面组件 │ ├── components // 公共组件 │ └── utils // axios实例、token工具 └── vue.config.js前后端分离项目的工程组织是一门学问尤其是在多人协作或者后期维护时一个合理的目录结构能省下大量沟通成本。2. 技术选型背后的考量为什么是这套组合2.1 SpringBoot的价值把项目从“配置地狱”里解放出来在SpringBoot之前搞一个SpringMVC项目要写一堆XML配置、配置数据源、配置事务管理、配置视图解析器任何一个环节写错项目启动直接报错。SpringBoot把绝大多数配置通过约定俗成的方式自动化了内置Tomcat加依赖就能跑大幅降低了Spring家族的使用门槛。这个项目选择SpringBoot 2.7.x核心原因有三点第一这个版本的依赖管理比较稳定网上公开的资料最多遇到兼容性问题时基本能搜到答案第二内嵌Tomcat让Jar包可以直接通过java -jar启动配合Nginx做静态资源代理部署流程极其简单第三SpringBoot对MyBatis、MySQL、Redis这些主流组件的自动配置支持非常完善省去了大量样板配置代码。2.2 MyBatis为什么会是中小型项目的最佳选择之所以在这个项目里选择MyBatis而不是JPA或者MyBatis-Plus主要是想让大家真正掌握SQL的编写能力。现在很多开发者从MyBatis-Plus入门写代码全是selectList、selectById连最简单的多表关联查询都要去搜索引擎现搜SQL语法这是很危险的。MyBatis的核心价值在于“SQL与代码分离”。SQL写在XML文件里意味着我可以随时把生产环境里的复杂SQL拿过来调整、优化、测试而不需要重新编译Java代码。在这个项目里订单列表的分页查询、分类商品的按条件搜索、销售统计的聚合查询我都写了完整的SQL实现每条SQL都值得反复看几遍。另外MyBatis还有一个容易被人忽略的功能——动态SQL。比如商品列表搜索用户可能只选择了分类、价格区间、关键词中的某一个条件用where加if标签可以实现条件自适应的SQL拼接这个能力在写C端筛选接口的时候极其好用。我在后面的小节会给出具体例子。2.3 MySQL表结构设计中的几个关键点社区团购系统的数据表不算多核心就是五张表user用户表、category商品分类表、product商品表、cart购物车表、orders订单表。另外加了两张辅助表order_detail订单明细表和banner首页轮播图表。表结构设计上有几个细节值得展开讲一下。商品表在设计时需要考虑不同商品的不同规格问题。如果是纯社区团购的果蔬生鲜类商品单个商品通常只有一个价格所以设计时直接在product表里放price和stock两个字段就够了。但如果以后要拓展到服装类商品就需要拆出sku表来区分颜色、尺码、价格、库存这个属于系统演进过程中的迭代设计。订单表的设计则需要注意一个关键问题用户下单时必须把商品名称、商品图片、购买单价、购买数量这些信息冗余到order_detail表中。这是因为用户查看历史订单时需要的价格应该是下单那一刻的价格而不是商品现在的价格。如果通过商品表去关联查询一旦后台改了商品价格历史订单的数据就对不上了。这种冗余设计在电商系统里叫“快照”是业务流程中一个很重要的细节。订单状态字段我用的是status整数类型枚举状态值含义用户端显示0待付款去支付1待发货等待商家发货2待收货查看物流 / 确认收货3已完成查看详情4已取消订单已取消3. 核心功能实现从零到一搭起完整业务闭环3.1 后端工程搭建与基础配置后端基础工程我使用了Spring Initializr生成核心依赖包括spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、lombok、jjwtJava JWT库等。application.yml里的几个关键配置如下server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/community_group_buying?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiautoReconnecttrue username: root password: root jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里有两个细节值得新手注意。一是数据库连接URL里一定要带上serverTimezoneAsia/Shanghai否则高版本MySQL驱动会因为时区问题抛异常。二是map-underscore-to-camel-case: true这个配置它能把数据库表里的user_name自动映射到实体类的userName字段省去大量手写resultMap的样板代码。3.2 统一响应对象与全局异常处理前后端分离项目中前后端的沟通协议必须保持一致。如果后端有的接口返回{code:0, data:...}有的接口返回{success:true, result:...}前端封装时会非常痛苦。所以第一步就统一定义了响应对象RData public class R { private Integer code; private String msg; private Object data; public static R ok() { ... } public static R ok(Object data) { ... } public static R error(String msg) { ... } public static R error(Integer code, String msg) { ... } }在此基础上全局异常处理器把所有业务异常、参数校验异常、系统异常统一转换为R对象返回。这样前端只需要在axios的响应拦截器里处理code 200这个分支即可。3.3 商品列表的分页与条件查询实现商品列表是C端高频访问的接口需要同时支持分页、分类筛选、价格排序、关键词搜索等多个参数。这里用到了MyBatis动态SQL的能力select idselectProductPage resultTypecom.community.entity.Product SELECT p.*, c.name as category_name FROM product p LEFT JOIN category c ON p.category_id c.id where p.status 1 if testcategoryId ! null and categoryId ! 0 AND p.category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (p.name LIKE CONCAT(%, #{keyword}, %) OR p.description LIKE CONCAT(%, #{keyword}, %)) /if /where if testsortType 1 ORDER BY p.price ASC /if if testsortType 2 ORDER BY p.price DESC /if if testsortType null or sortType 0 ORDER BY p.id DESC /if /select这个SQL里如果某个参数为null对应的查询条件就不会拼接进去MyBatis会自动调整SQL。对于C端这种多条件组合筛选的场景比Java代码里拼SQL字符串要安全得多也能避免SQL注入风险。分页我这里用的是最传统的方式——先查询总条数再查当前页数据。项目里我没有引入PageHelper原因是这个项目数据量并不大手动处理分页逻辑反而能帮助理解分页原理面试时被问到“分页是怎么实现的”也能更有底气地讲清楚。3.4 订单提交的分布式事务思考订单提交是电商系统最核心、最容易出问题的环节。一个用户点击“提交订单”后端需要做这些事情校验商品库存、扣减库存、创建订单主表记录、创建订单明细记录、清空购物车中已结算的商品。Java代码实现中我把整个流程用Transactional注解包裹以保证上述操作要么全部成功要么全部回滚。比如用户提交订单后如果创建订单明细失败之前扣减的库存必须回滚否则就会造成“库存已经扣了但订单不存在”的严重数据一致性问题。真实互联网环境下订单系统还会引入消息队列如RocketMQ、RabbitMQ做异步解耦但由于本项目是单体架构数据库事务已经能满足需求。这一点也是面试拓展的好话题分布式事务的最终一致性方案为什么需要消息队列本地消息表又是怎么解决的。3.5 模拟支付流程的技术设计真实支付需要接入微信支付/支付宝支付涉及商户号申请、证书下载、回调接口等一系列流程。个人开发者很难完成这些流程所以项目中使用“模拟支付”方案用户点击支付按钮前端调用后端/order/pay/{orderId}接口后端模拟银行网关扣款将订单状态从“待付款”改为“待发货”并写入支付流水记录。模拟支付虽然简单但支付回调的幂等性设计必须体现出来。如果用户重复点击支付按钮同一个订单不能产生两条支付流水也不能重复更新订单状态。这里我通过在payment表中对order_id添加唯一索引来保证幂等如果插入重复的支付记录数据库会抛出唯一约束异常被全局异常处理器捕获后提示“订单已支付”。这个设计思路和真实支付回调处理是完全一致的。4. 前端工程Vue全家桶与后端接口的深度协作4.1 axios封装与请求拦截器、响应拦截器前端所有HTTP请求都必须经过统一的axios实例。封装的核心目的是统一处理Token注入、错误提示、登录过期跳转等逻辑避免每个页面组件里都写一遍重复代码。import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: /api, timeout: 15000 }) // 请求拦截器自动携带 Token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }, error Promise.reject(error)) // 响应拦截器统一处理业务状态码 service.interceptors.response.use(response { const res response.data if (res.code 401) { localStorage.removeItem(token) router.push(/login) Message.error(登录已过期请重新登录) return Promise.reject(new Error(unauthorized)) } if (res.code ! 200) { Message.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { Message.error(网络异常请稍后重试) return Promise.reject(error) }) export default servicebaseURL: /api这里的处理很有讲究。开发环境下通过vue.config.js的devServer.proxy把/api开头的请求代理到http://localhost:8080这样本地开发时不需要处理跨域问题。生产环境部署时Nginx层再做一次反向代理把/api转发到后端Java服务端口。通过这种配置前端代码里的接口地址就可以保持相对路径不会以后端地址迁移而大面积报错。4.2 JWT Token的生成、校验与刷新机制用户登录成功后后端生成一个JWT Token返回给前端。Token里封装了用户ID、用户名和角色信息并通过HMAC-SHA256算法签名。实现中用到了jjwt库核心代码如下public String generateToken(User user) { Date now new Date(); Date expiryDate new Date(now.getTime() 7 * 24 * 60 * 60 * 1000); // 7天有效期 return Jwts.builder() .setSubject(user.getId().toString()) .claim(username, user.getUsername()) .claim(role, user.getRole()) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS256, jwtSecret) .compact(); }请求到达业务Controller之前先经过拦截器。拦截器从请求头的Authorization字段取出Token解析并校验签名和过期时间。如果校验失败统一返回401状态码。这里要特别注意拦截器在preHandle中必须放行登录、注册等公开接口可以通过路径匹配的方式排除Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/**) .excludePathPatterns(/user/login, /user/register, /product/**, /category/**, /banner/**); }JWT的过期时间设置是7天。对于社区团购这种业务场景来说7天已经是比较合理的周期。如果要做微信小程序/App客户端可以额外设计Refresh Token机制但Web端项目7天有效期的方案已经足够用。4.3 Vue Router路由守卫与页面权限控制前端路由根据用户角色分为两类普通用户可访问的C端页面和管理员专属的B端页面。路由守卫里通过读取Vuex中的userInfo.role字段判断当前用户是否具备访问权限router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() return } if (!token) { next(/login) return } if (to.meta.requiresAdmin) { const role store.state.user.userInfo?.role if (role ! 1) { Message.error(无权限访问) next(/) return } } next() })这种前端路由守卫有时候会被说“不安全”因为用户可以通过修改本地存储的角色数据来绕过检查。但它的价值在于用户体验层面防止用户进入与自己无关的页面。真正的权限安全必须依靠后端接口层面的拦截器校验这两者是互相配合的关系前端做展示层控制后端做数据层控制。4.4 购物车与订单确认页的复杂状态管理购物车页面是C端交互最复杂的模块涉及商品选择、数量增减、单选/全选、合计金额计算等多个状态。这里用Vuex管理购物车数据页面的每个操作都通过mutation同步更新store然后重新计算选中商品的总金额。Vuex中的checkedItems这个字段专门记录选中的购物车商品ID列表。用户点击全选/取消全选、单选/取消单选时都会触发对应的mutation更新这个列表。结算按钮点击时后端接收购物车ID列表和用户优惠信息重新计算价格后创建订单。为什么结算要由后端重新计算价格而不是直接信任前端传过来的金额因为前端传过来的任何数据都可能是被篡改过的商品单价、合计金额这些关键数值必须由后端从数据库查出后在Service层计算才能保证安全性。这个“永远不要信任前端数据”的思维方式是后端开发入门的必修课。5. 数据库设计脚本与MyBatis操作细节5.1 完整建库建表SQL脚本数据库命名为community_group_buying字符集使用utf8mb4。重点说明一下为什么用utf8mb4而不是utf8MySQL中的utf8最多支持3字节字符存储不了生僻字和emoji表情而utf8mb4是完整的UTF-8实现兼容性更好。CREATE DATABASE community_group_buying DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE community_group_buying; CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录用户名, password VARCHAR(255) NOT NULL COMMENT BCrypt加密后的密码, nickname VARCHAR(50) COMMENT 昵称, avatar VARCHAR(255) COMMENT 头像地址, phone VARCHAR(20) COMMENT 手机号, role TINYINT DEFAULT 0 COMMENT 0-用户 1-管理员, status TINYINT DEFAULT 1 COMMENT 1-正常 0-禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); CREATE TABLE category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort_order INT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT NOT NULL, name VARCHAR(100) NOT NULL, main_image VARCHAR(255), detail TEXT, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, sales INT DEFAULT 0, status TINYINT DEFAULT 1 COMMENT 1-上架 0-下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_category_id (category_id) ); CREATE TABLE cart ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1, checked TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_product (user_id, product_id) ); CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL UNIQUE COMMENT 订单编号, user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, pay_amount DECIMAL(10,2) NOT NULL, pay_type VARCHAR(20) DEFAULT wechat, status TINYINT DEFAULT 0, receiver_name VARCHAR(50), receiver_phone VARCHAR(20), pickup_address VARCHAR(255), pay_time DATETIME, deliver_time DATETIME, finish_time DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_id (user_id), INDEX idx_status (status) ); CREATE TABLE order_detail ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, product_name VARCHAR(100), product_image VARCHAR(255), product_price DECIMAL(10,2), quantity INT, INDEX idx_order_id (order_id) ); CREATE TABLE payment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, user_id BIGINT NOT NULL, pay_amount DECIMAL(10,2), pay_type VARCHAR(20), pay_status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_id (order_id) );在设计cart表时我加了UNIQUE KEY uk_user_product (user_id, product_id)唯一索引。用户在购物车里点击同一个商品两次时后端不再去看数据库里是否重复而是直接利用唯一索引冲突再把查询和更新转换成一条INSERT ... ON DUPLICATE KEY UPDATE quantity quantity 1语句这样可以避免先查询再更新的并发问题。5.2 MyBatis易错点单个数字与字符串比较搜索热词里有一条“mybatis 单个数字字符比较”这个问题在实际开发中确实出现过而且非常迷惑。MyBatis中的if teststatus ! null and status ! 这里如果status是整型数字一旦写成status ! MyBatis会走OGNL表达式求值在部分版本中会隐式地做字符串转换导致预期外的结果。典型例子if teststatus ! null and status ! AND p.status #{status} /if当status为数字0时OGNL会将0转成字符串0然后再与空字符串比较此时判断结果可能是false导致SQL里少拼了条件最终查询出来的数据是错的。解决办法就是分类进行处理整型字段的判断只写! null不要追加! 字符串字段的判空才使用! null and ! 。if teststatus ! null AND p.status #{status} /if这种细节问题不出现一次真的很难从资料上学到。代码在本地测不出来但是到生产环境数据量大了之后某个特定条件组合下结果数据就不对了排错成本特别高。5.3 MyBatis一二级缓存的基本认知关于MyBatis缓存这个项目里我只保留了默认的配置并没有做额外的缓存策略。但既然面试常问也简单说下MyBatis的缓存机制一级缓存是SqlSession级别的本地缓存同一个SqlSession中执行相同的查询会直接返回缓存结果二级缓存是Mapper级别的缓存可以被多个SqlSession共享但要注意缓存的失效条件和数据一致性。在开发这个项目的过程中我建议把二级缓存关闭默认为不开启。原因很简单社区团购这种场景下商品库存、订单状态都是实时变化的数据如果开启缓存很容易出现用户查到的商品库存是旧缓存的情况。为了在界面显示“仅剩3件”的效果库存数据需要尽量实时所以这个项目里完全没有引入Redis做热点数据缓存也是基于业务实时性的考虑。存储过程在这个项目中也没有使用。有些人喜欢把复杂的业务逻辑写成存储过程放在数据库里但维护起来非常痛苦。我倾向于把业务逻辑放在Java代码中控制数据库只做简单的增删改查和基础约束这样后续做代码审查、调试、单元测试都更容易。6. 前后端分离项目的部署上线指南6.1 本地开发环境启动完整流程本地启动这个项目需要提前安装好以下工具工具版本要求作用JDK1.8编译运行SpringBootMaven3.6后端依赖管理与打包MySQL5.7 / 8.0数据库服务Node.js14.x / 16.x前端依赖安装与构建npm / yarn任意常用版本前端包管理器后端启动流程先执行database.sql脚本完成建库建表然后修改application.yml中的数据库用户名密码最后运行mvn spring-boot:run或者直接在IDE中运行启动类。后端启动成功的标志是控制台出现Started CommunityApplication并且8080端口正常监听。前端启动流程进入frontend目录执行npm install安装依赖这里建议用淘宝镜像源速度快很多然后执行npm run serve启动开发服务器。默认端口是8081此时浏览器访问http://localhost:8081即可进入项目首页。6.2 使用Nginx反向代理部署前后端生产环境部署时前端打包后变成一堆静态文件HTML/CSS/JS后端是一个可执行的Jar包。最常用的部署方式是Nginx托管前端静态资源同时反向代理后端接口。前端打包命令是npm run build打包产物在dist目录。将这个目录里的所有文件上传到服务器比如放在/var/www/community目录下。Nginx配置如下server { listen 80; server_name yourdomain.com; # 前端静态资源 location / { root /var/www/community; index index.html; try_files $uri $uri/ /index.html; # 处理Vue Router history模式 } # 后端API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }后端Jar包通过mvn package打出来或者在Linux上执行mvn clean package -DskipTests。然后使用nohup java -jar community-backend.jar app.log 21 后台启动即可。注意生产环境的数据库配置直接用环境变量注入不要把密码硬编码到配置文件里。前端部署还有一个容易被忽略的地方Vue Router默认是哈希模式URL中带#为了让URL更好看在vue.config.js/router中开启mode: history后也需要配合Nginx的try_files配置。如果忘记配置用户刷新/product/123这个页面时Nginx找不到对应的文件会返回404这个问题在部署时经常出现一定要提前配好。6.3 服务器环境准备说明如果在阿里云、腾讯云等云服务器上部署准备工作如下在安全组规则中放行80端口HTTP和8080端口后端服务安装JDK、MySQL、Nginx按需启动服务。MySQL数据库连接建议在服务器上直接导库不要开放公网3306端口尽量避免安全风险。后端服务的端口绑定建议为0.0.0.0否则可能无法从公网访问。7. 常见问题与排查技巧实录7.1 常见问题速查表下面这些坑是我在开发、调试这个项目的过程中真实遇到过的每一项都花了不少时间排查。整理成表格方便大家直接检索问题现象根本原因解决方案前端访问接口报CORS跨域错误开发环境未配置代理或生产环境Nginx未做反向代理开发环境配置devServer.proxy生产环境配置location /api/转发接口返回401浏览器没有跳转登录页响应拦截器的res.code 401处理分支遗漏了或者状态码放在HTTP header里没走到业务返回确认后端未登录统一返回HTTP 401还是业务code 401前后端约定保持一致商品图片加载不出来图片路径是后端的相对路径前端请求的域名/IP不一致数据库中图片字段存相对路径前端拼接图片服务器的基础URL或者后端配静态资源映射数据库中文乱码建库时字符集用成了utf8部分生僻字无法存储连接URL缺少characterEncodingutf8统一使用utf8mb4连接URL带上字符集参数登录接口在Postman测试正常前端访问500前端请求未携带Content-Type后端参数接收失败axios默认会自动处理检查是否覆盖过headers配置java.lang.IllegalStateException: Cannot call sendError过滤器或者拦截器中出现循环转发检查拦截器对/error路径是否也做了Token校验需要放行错误转发路径打包集成后图片/JS文件404dist目录中静态资源路径为绝对路径assets/...部署到子路径下路径不匹配vue.config.js中设置publicPath: ./使用相对路径使用MyBatis的符号报错XML中会被解析为标签开头使用XML转义符lt;或写入SQL时用![CDATA[ ]]用户删除后历史订单查询报错关联查询使用inner join用户删除后订单查不到使用LEFT JOIN订单查询时以订单数据为主表7.2 排查技巧实录前后端联调时的高效定位方法前后端分离项目调试时如果接口返回异常建议按照“网络层 → 后端日志 → 业务层”的顺序逐步定位。首先打开浏览器开发者工具F12查看Network标签页中请求的状态码、请求参数、响应数据。如果接口返回500直接去后端控制台看异常堆栈。开发过程中我习惯在后端配置mybatis.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样每次执行SQL都能在控制台看到完整的SQL语句和传入的参数排查SQL拼接错误、参数缺失等问题时非常直观。遇到前端参数传过来但后端接收不到的情况优先检查三项内容一是后端实体类的属性名与前端提交的字段名是否一致二是是否缺少RequestBody注解三是前端是否真的把数据传到了请求体里而不是放在了URL参数中。7.3 关于Vue与SpringBoot版本兼容性的避坑建议最后说下版本选择的问题。我在项目里使用的是Spring Boot 2.7.x 与 Vue 2.6.x这两个版本搭配非常默契。Spring Boot 3.x 要求JDK 17、Jakarta EE规范MyBatis、Spring Security等生态的兼容性需要额外适配Vue 3 Vite 虽然性能更好但组件库、插件生态与Vue 2 存在较大差异老项目升级改造成本不低。如果是从这个项目入门、或者准备做毕业设计我建议先用Spring Boot 2.7 Vue 2这套稳定组合跑通整个流程后面再逐步升级。Maven依赖版本冲突是SpringBoot开发中最容易让人抓狂的问题之一。比如spring-boot-starter-parent的版本管理已经指定了MySQL驱动的版本但如果你手动引入mysql-connector-java时又写了一个不同的版本号就会出现No suitable driver之类的奇怪报错。处理办法是能不加版本号就不加尽量使用父级依赖管理统一控制版本。8. 这个项目后续还能怎么扩展项目做完之后有几个方向可以继续拓展。第一个方向是引入Redis缓存热点数据。目前商品列表每次查询都要走数据库后续如果商品数量变大、用户访问量上来可以把首页分类、商品详情等热点数据缓存到Redis配合Spring Cache注解可以做到对业务代码零侵入。登录态的管理也可以改成JWT无状态方案和Redis缓存方案结合实现Token主动失效能力。第二个方向是做秒杀/限时购功能。社区团购经常有定时开团秒杀活动这个场景需要额外处理超卖问题和接口限流问题是很好的技术挑战。可以研究一下Redis的原子性扣减库存、分布式锁、消息队列削峰等方案扩展的想象空间很大。第三个方向是引入MyBatis-Plus提高开发效率。现在的项目接近原生MyBatis实现很适合学习SQL底层原理。如果日常开发追求效率可以在此基础上引入MyBatis-Plus把通用CRUD交给框架搞定只保留复杂查询写XML两种搭配是当前企业项目的主流形态。从我个人的实际经验来说这个项目最值得研究的地方其实不只是代码本身而是整个前后端分离的开发模式怎么设计接口协议、怎么管理Token、怎么处理跨域、怎么打包部署。这些能力是“会不会写代码”和“能不能独立负责一个项目”的分水岭。把这套链路完整跑通一遍后面再写其他任何业务系统思路都会清晰很多。