Spring Boot+Vue宠物用品商城实战:从SSM到前后端分离部署
我先说点实在的。做Java后端这几年被问得最多的问题之一就是ssm、springboot、vue这仨到底什么关系宠物用品商城这类电商项目该怎么从零搭起来尤其是很多准备毕业设计、或者准备跳槽做全栈的兄弟一搜资料全是老掉牙的JSPServlet要么就是只贴代码不讲为什么的缝合怪教程。我自己带过好几个从SSM硬切Spring Boot的项目组也踩过Vue前后端联调的大坑这篇就把我实际做宠物用品商城积累的经验完整拆一遍从框架选型到核心代码再到打包部署全部摊开讲。如果你正在做基于Spring Boot Vue的电商类Java毕设或者想搞懂为什么现在的Java项目都默认用Spring Boot这篇能让你少走很多弯路。1. 项目整体设计与技术选型拆解1.1 SSM和Spring Boot到底什么关系很多人一看标题“ssm springboot宠物用品商城电子商务vue”就懵了ssm和springboot不是一个东西吗怎么会同时出现这其实是很多项目资料上的历史遗留写法。SSM指的是Spring SpringMVC MyBatis这套经典组合而Spring Boot本质上是Spring生态的“一键启动器”它把SSM里的Spring和SpringMVC整合进了同一个框架再帮你省掉大量XML配置。我拿实际开发场景举个例子。传统SSM搭一个能跑通的项目你得先配web.xml、spring-config.xml、springmvc-config.xml、mybatis-config.xml还要处理各种jar包版本冲突光是让项目启动不报错就能折腾一两天。而Spring Boot用自动配置AutoConfiguration机制把从前手动配置的事情交给框架自己判断。你在pom.xml里引入了spring-boot-starter-web框架就自动帮你把内嵌Tomcat、DispatcherServlet、JSON转换器全部装配好你只需要写业务代码。但话说回来如果你在培训机构或者学校教材里学过SSM那些知识完全没有浪费。Spring Boot只是把SSM的配置方式简化了底层的Bean管理、AOP切面、MyBatis的Mapper代理机制、SpringMVC的请求映射流程这些核心原理一模一样。所以我说这是一个“旧知识新姿势”的迁移过程你的SSM底子越扎实用Spring Boot就越顺手。1.2 为什么宠物用品商城适合用Vue做前端商城这类项目有个特点页面状态多、交互逻辑复杂、数据实时性要求高。购物车要随时加减数量、商品列表要支持多条件筛选、订单状态要即时刷新如果用传统的服务端模板渲染JSP、Thymeleaf页面每一次局部变化都得刷新整个页面或者用Ajax拼HTML代码又乱又难维护。Vue解决的核心问题是数据驱动视图。你不需要手动操作DOM去改页面元素只需要维护一个JavaScript数据对象比如购物车列表cartList当用户点“加入购物车”时Vue会自动把新增的数据渲染到页面上。这一点对商城项目简直是救命级别的方便尤其是商品筛选、排序、购物车角标这些高频交互。另外Vue的单文件组件.vue文件把HTML、CSS、JavaScript封装在一起对于一个宠物用品商城来说你可以把“商品卡片”、“价格区间筛选器”、“订单状态标签”这些通用模块都做成组件在首页、列表页、搜索结果页反复复用。我实测下来如果纯用JQuery写一个功能完整的商城前端代码量至少是Vue的三倍而且后期加需求改起来非常痛苦。1.3 技术栈选型的核心权衡考虑到这是很多人的毕设或者个人项目我建议完整技术栈这样配层级技术选型选择理由后端框架Spring Boot 2.7.x生态最成熟资料最多避免3.x版本推坑ORM层MyBatis-Plus单表CRUD不用写SQL复杂查询保留XML能力数据库MySQL 5.7/8.0免费稳定商城业务完全够用权限认证JWT Spring Security前后端分离标准方案状态无session化前端框架Vue 2.7 / Vue 3 Element UI组件库完善后台管理页开发效率极高构建工具Maven 3.6 / npm 8Java和前端生态标配文件存储Minio本地部署 / 阿里云OSS商品图片上传必用Minio免费可控这里我特别想提醒一个点不要盲目追求最新版本。Spring Boot 3.x要求JDK 17以上很多老教程和毕设资料还在用JDK 8你如果跟着视频做版本一高环境对不上光解决环境兼容问题就能耗掉好几天。我自己的项目直接锁定Spring Boot 2.7.14 JDK 8 Vue 2.7半年多没碰过版本兼容的坑。做项目前期一定要把“贯彻到底”当成目标赌版本新不如赌资料全。2. 核心业务需求解析与功能模块设计2.1 宠物用品商城的角色权限划分商城的用户体系比较清晰主要分三种角色消费者前台用户、商家/管理员后台运营、超级管理员系统维护。这里我建议用RBAC基于角色的访问控制模型给每种角色分配不同的菜单权限和数据权限。前台用户能看到商品列表、加入购物车、下单付款、查看个人订单详情后台管理员能管理商品上下架、处理订单发货、修改库存超级管理员除了后台管理权限还能管理前台注册的用户账号、冻结异常账户。有些技术资料喜欢做非常复杂的权限模型甚至用上Shiro加一套完整的权限分配界面但一个宠物用品商城的毕设真没必要搞那么重两张表用户表角色表加上JWT拦截器就完全够用。另外一点值得注意商品要区分“上架/下架”状态而不是直接从数据库删除。我见过很多新手做后台删除商品直接在数据库把记录delete掉这样订单表和购物车表里关联的商品信息就全部悬空后面查订单详情必然NPE。正确做法是设计一个status字段取值范围0下架、1上架、2已删除删除只是改状态数据可回溯。2.2 商城核心流程从商品浏览到订单完成一个最小可用闭环是用户注册登录 → 首页/分类页浏览商品 → 点击商品进入详情 → 加入购物车 → 购物车勾选结算 → 生成订单 → 模拟支付 → 管理员后台发货 → 用户确认收货。这九个步骤把电商业务的主干全部覆盖每一步对应后端的若干个接口和前端页面。就拿“加入购物车”这一个看似简单的操作来说它牵扯到的后端逻辑至少有判断用户登录状态、检查商品是否存在且为上架状态、查询当前库存是否足够、判断购物车是否已存在同一商品存在则数量累加不存在则新增一条、最后返回购物车最新数量。如果你不用Vue管理购物车状态纯靠后端来回传参用户稍微手快点个两三次数量就乱了。这也是前面说必须用Vue的重要原因。订单这块我建议一开始就考虑到“订单状态机”。从代码层面不要用普通的整型字段随意set值而是用枚举定义好状态流转关系待支付(0) → 已支付(1) → 已发货(2) → 已签收(3) / 已取消(-1)。状态机的好处是防止非法状态跳转比如用户不能把“待支付”直接改成“已签收”必须经由后端校验和业务操作来驱动状态变化。2.3 数据库表设计的关键细节商城项目的数据库表大概有10~15张我按实际业务拆分如下用户表(user)user_id、username、password加密存储、nickname、avatar、phone、role_id、status、create_time商品分类表(category)category_id、parent_id、name、sort_order商品表(product)product_id、category_id、name、sub_title、main_image、detail_image、price、stock、sales、status、create_time购物车表(cart)cart_id、user_id、product_id、quantity、checked、create_time订单表(order)order_id、order_no(唯一订单号)、user_id、total_amount、pay_amount、status、create_time、pay_time订单明细表(order_item)item_id、order_id、product_id、product_name、product_image、price、quantity、total_price这里有个很多新手容易踩的坑商品表里不要只存一个图片URL而是存主图和轮播图两个字段。比如main_image是封面图detail_image存放逗号分隔的多张图片地址字符串。这样列表页只取封面图详情页再按逗号拆开渲染轮播图性能好、实现简单。如果你把图片存成一张独立的商品图片表也不是不行但查询时得多一次join毕设级别没必要把复杂度提上去。订单号必须用服务器生成的唯一字符串我推荐用时间戳随机数用户ID拼接的方式例如202503150930451234567899001确保并发条件下不会重复。不要用数据库自增ID当订单号一旦暴露会给用户猜测订单量留下漏洞而且多表分散后id冲突问题很难查。3. 核心技术与实操要点3.1 从SSM到Spring Boot自动配置与注解驱动的迁徙既然标题挂了ssm这一步我单独展开讲。SSM时代写Controller你需要同时写一个Controller注解和一个ResponseBody注解或者类上写RestController并且在xml里手动配置组件扫描包路径。Spring Boot把这些事情全部规范化了RestController直接等于ControllerResponseBody启动类上的SpringBootApplication自带包扫描、自动配置、属性绑定三个能力。下面这段代码是Spring Boot宠物用品商城后端最常见的Controller写法我把注释写细一点RestController RequestMapping(/api/product) public class ProductController { Autowired private ProductService productService; // 分页查询商品列表GET /api/product/list?pageNum1pageSize8categoryId1 GetMapping(/list) public ResultPageInfoProductVO list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 8) Integer pageSize, RequestParam(required false) Integer categoryId) { // PageHelper一行代码起分页底层原理是MyBatis拦截器自动拼接LIMIT语句 PageHelper.startPage(pageNum, pageSize); ListProductVO products productService.listByCategory(categoryId); return Result.success(new PageInfo(products)); } // 查询商品详情GET /api/product/detail/101 GetMapping(/detail/{productId}) public ResultProductVO detail(PathVariable Integer productId) { return Result.success(productService.getDetail(productId)); } }注意几个关键点统一返回结果用Result对象不要直接返回实体类或者散装的Map。ResultT里固定包含code、message、data三个字段前端Axios拦截器统一判断code是否等于200。这样做的好处是前端不需要在每个请求里单独处理错误分支后端抛异常统一由RestControllerAdvice全局捕获用户体验和代码整洁度都高很多。PageHelper.startPage 必须放在查询语句的第一行它底层是通过ThreadLocal对紧接着的下一条MyBatis查询生效。如果你在分页查询前又执行了别的查询分页就会串数据这个坑我在真实项目中碰到过不止一次。不要用基本类型接收前端传递的参数一律用包装类型。比如这里的Integer而不是int否则前端少传参数时后端直接抛MissingServletRequestParameterException排查问题会很绕。3.2 MyBatis-Plus代码生成与复杂查询引入MyBatis-Plus后单表CRUD基本不用写SQL了。像商品表这种常用表直接让实体类继承BaseMapperProductCRUD方法直接从父类拿来用。比如新增商品Autowired private ProductMapper productMapper; public int addProduct(Product product) { // 内置方法自动拼接INSERT主键默认返回自增id return productMapper.insert(product); }但商城项目不是所有SQL都能用内置方法搞定。比如按照多个条件动态筛选商品传统写法要拼接SQL字符串MyBatis-Plus提供了QueryWrapper不过我更推荐直接用Select注解写在Mapper接口上。我自己习惯把复杂查询写在XML文件里因为后期维护直观而且多表联查时XML的sql片段复用能力很强。动态SQL是另一大重点。商品列表的筛选条件分类、品牌、价格区间、关键词不是固定的用户可能只填一个也可能全填。如果你用字符串拼接的方式去拼SQL稍不注意就会有SQL注入风险。MyBatis的if标签是设计好的安全拼法它会在执行前做参数预编译。比如select idsearchProducts resultTypecom.petmall.vo.ProductVO SELECT * FROM product WHERE status 1 if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if if testminPrice ! null AND price #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if ORDER BY create_time DESC /select老手和新手在写动态SQL上有个明显区别新手会把所有条件都写在一条SQL里导致索引失效老手会先根据业务拆分查询场景再针对热点场景创建复合索引SQL上尽量用覆盖索引。比如宠物食品分类下的价格排序你可以在(category_id, price, status)上建一个联合索引让查询直接命中索引不回表性能提升是肉眼可见的。3.3 图片上传与Minio接入商品图片上传是商城必备功能我建议用Minio而不是把图片硬编码到项目里。Minio是一个开源的、兼容亚马逊S3协议的对象存储服务本地部署只要一个jar包就能跑起来中文资料也不少。你想想如果用传统的目录存储——把图片写到项目下的static/upload目录——本地开发看不出问题但每次重新打包部署老图片就没了而且线上多实例部署时文件各存各的用户看到的商品图会时有时无。接入Minio的核心步骤是三步引入依赖、配置连接参数、写上传工具类。# application.yml 中的Minio配置 minio: endpoint: http://127.0.0.1:9000 access-key: admin secret-key: admin123456 bucket-name: pet-mall-imagesService public class MinioService { Value(${minio.endpoint}) private String endpoint; Value(${minio.bucket-name}) private String bucketName; private MinioClient minioClient; PostConstruct public void init() { // PostConstruct保证项目启动完成后自动创建连接客户端 minioClient MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); // 检查bucket是否存在不存在则创建省得每次上传前写判断 boolean exists minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); if (!exists) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } } public String upload(MultipartFile file) { String fileName UUID.randomUUID().toString().replace(-, ) _ file.getOriginalFilename(); // object名称用UUID原文件名避免用户上传重名文件互相覆盖 minioClient.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(fileName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return endpoint / bucketName / fileName; } }这里有个安全细节不要把Minio的管控台端口和API端口混为一谈。默认API端口是9000控制台是9001前端如果直接访问9000端口拼路径是可以拿到的。但给用户展示图片的URL尽量通过后端接口统一转发不要直接暴露bucket名和AccessKey权限否则别人能列举你的文件目录。网上有很多Minio权限配置不当被刷流量的案例钱吃亏事小数据泄露是大问题。3.4 Vue前端核心路由、状态管理与Axios封装Vue负责商城前端的交互。如果你用Vue CLI创建项目核心要理解三个概念路由vue-router、状态管理Vuex/Pinia、网络请求axios。路由解决“页面怎么跳转”状态管理解决“数据在组件间怎么共享”axios解决“怎么和后端对接”。菜鸟最容易出错的地方就在路由。商城的前台页面和后台管理页面要分开设计不要都挤在同一个路由表里。前台路由用/shop前缀后台路由用/admin前缀后台整体套在一个需要登录权限的父路由下通过路由守卫统一判断// router/index.js const router new VueRouter({ routes: [ { path: /login, component: Login }, { path: /admin, component: AdminLayout, redirect: /admin/dashboard, children: [ { path: product/list, component: ProductList }, { path: order/list, component: OrderList } ], meta: { requiresAuth: true } // 标记需要登录 } ] }) // 全局前置守卫没有token就跳登录页 router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })状态管理我单独强调一下购物车数据别再放各个组件内部了直接放在Vuex里。用户加了购物车后角标要变购物车页面要显示结算页要用汇总金额这三个地方都要消费同一份数据。你用一个全局状态管理任意组件里this.$store.dispatch(addToCart, product)一下所有页面自动同步代码写起来又干净又不容易出bug。Axios封装是项目起步阶段就该做好的事情。别在每一个页面里直接调用axios.get最好封装一个request.js统一处理baseURL、超时时间、请求头token、响应拦截器。我吃过亏一开始每个页面自己写ajax后来要统一加token和错误提示如果没封装几百个请求你要一个一个改想死的心都有。封装示例// utils/request.js import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, // 开发环境走代理生产环境走nginx timeout: 10000 }) // 请求拦截器自动带token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) // 响应拦截器统一处理业务码和登录过期 service.interceptors.response.use( response { const res response.data if (res.code 200) { return res } // 业务异常直接弹提示 Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) Message.error(登录已过期请重新登录) } else { Message.error(网络异常请稍后重试) } return Promise.reject(error) } )3.5 前后端分离部署使用Nginx与Spring Boot的通信开发环境前端跑在http://localhost:8080后端跑在http://localhost:8088必然存在跨域问题。解决跨域的常规手段有三类后端加CrossOrigin注解、配置CorsFilter、前端用Vite/Vue CLI的proxy代理。三种我都用过这里说我的经验后端加注解是最省事的但这只适合开发环境生产环境前端和后端域名一致靠Nginx反向代理解决根本不需要在后端处理跨域。一个典型的Nginx配置片段是server { listen 80; server_name petmall.example.com; # 前端静态资源 location / { root /usr/share/nginx/html/pet-mall-frontend; index index.html; try_files $uri $uri/ /index.html; # Vue路由history模式刷新不404 } # 后端API反向代理 location /api/ { proxy_pass http://127.0.0.1:8088; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里那个try_files是Vue Router用history模式的大坑。如果你不用hash模式地址栏带#号刷新一个非根路径比如/admin/product/list时后端找不到这个资源就会返回404。try_files就是告诉Nginx匹配不到具体文件时就请回index.html由前端路由自己接管。如果你嫌麻烦开发阶段直接用hash模式刷新问题彻底消失缺点就是地址栏带个#号看各自取舍。4. Spring Boot核心机制精讲4.1 自动装配原理与常用注解Spring Boot只要不报错就很神秘很多人只是会用——main方法里一个SpringApplication.run就能启动里面的魔法发生在哪一句话SpringBootApplication其实是三个注解的组合——SpringBootConfiguration标记为配置类、EnableAutoConfiguration开启自动装配、ComponentScan包扫描。真正的核心是第二个。EnableAutoConfiguration的运行逻辑是Spring Boot启动时扫描classpath下所有META-INF/spring.factories文件文件中列举了一堆XxxAutoConfiguration类。框架根据你引入的jar包和现有的配置状态按条件加载对应的自动配置类。比如你引入了spring-boot-starter-webclasspath下多了spring-webmvc这个包DispatcherServletAutoConfiguration中的条件匹配成功就会创建DispatcherServlet并注册到内嵌Tomcat——这就是你什么都不做也能发HTTP请求的原因。怎和Conditional深度绑定。框架据此判断是否创建某个Bean这就叫“条件装配”。所以你要写一个配置类让某个Bean在指定条件下生效就必须自己熟悉这些条件注解比如ConditionalOnProperty、ConditionalOnMissingBean。SSM项目中常见的Component、Service、Repository、Autowired在Spring Boot中照样用但有几个注解是新方案强烈建议的。比如ConfigurationProperties以前从properties文件读配置靠Value(${xxx.yyy})一个个给字段赋值现在你可以建一个配置类一次性批量映射Component ConfigurationProperties(prefix pet.mall) Data public class PetMallProperties { private String uploadPath; private Integer maxOrderCount; private String defaultAvatar; }加上Data后Spring Boot启动时会把application.yml中pet.mall前缀下的配置自动绑定到这个类的字段上。比写十几个Value清爽得多。还要提一个注解Transactional。订单接口必须加事务这是基本功。生成订单时需要扣库存、落订单主表、落订单明细表这三步必须是一个原子操作只要任一步失败就要全部回滚。新手最容易犯的错是只在Service里加注解却忘了事务默认只对RuntimeException生效如果你抛的是checked exception就不会回滚库存扣了但订单没插上亏到哭。4.2 Spring Boot项目结构规范一个规范的Spring Boot商城项目包结构我推荐这样划分com.petmall ├── controller # 控制层接收请求、参数校验、返回结果 ├── service # 业务层业务逻辑、事务控制 │ └── impl # 业务实现类接口和实现分离 ├── mapper # 数据访问层Mapper接口 XML文件 ├── entity # 数据库实体类与表字段一一对应 ├── vo # 视图对象专门返回给前端的数据结构 ├── dto # 数据传输对象接收前端传入的结构 ├── config # 配置类跨域、拦截器、WebMvc定制 ├── common # 公共类Result、异常处理、常量 ├── utils # 工具类JWT工具、Minio工具、订单号生成 └── enums # 枚举订单状态、角色类型entity、vo、dto三个千万不要混着用。我在代码评审中见过最头疼的情况有人把数据库实体直接返回给前端密码字段全部暴露还得在SQL里手动把password查出来再置空浪费时间且极其危险。正确姿势是数据库实体只在mapper层用业务层把Entity转成VO比如去掉密码、加上商品销量排序等Controller只接收DTO比如分页查询DTO、新增商品DTO用MapStruct或者手动BeanUtils.copyProperties转换都行。4.3 拦截器、过滤器和全局异常处理SSM时代的拦截器配置是一大堆xmlSpring Boot里只需要实现HandlerInterceptor接口再注册到WebMvcConfigurer里。商城项目中拦截器至少需要做两件事验证用户是否登录JWT校验、限制重复提交。JWT校验的逻辑很简单前端请求头带上token后端拦截器取出token并解析用户身份再把用户信息放到ThreadLocal中供业务层取用。Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); } try { Claims claims JwtUtil.parseToken(token); // 解析成功说明用户身份有效把userId放入request作用域 request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { // 设置响应状态为401前端拦截器收到后跳登录页 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } } }别小看全局异常处理。Controller里不要写一堆try-catch把异常处理的脏活都交给RestControllerAdvice。业务层抛业务异常比如“库存不足”、参数校验异常、未知运行时异常都由同一个类集中处理并转成标准Result格式返回。实际项目里统一异常处理还有一个隐藏好处线上排查问题时能通过日志链路追踪到具体方法因为每个异常都会被记录到同一个地方不至于在各业务代码里零星散落掩盖问题。5. 关键业务模块实现实录5.1 登录注册与JWT权限认证宠物用品商城的登录注册我强烈建议用JWT替代传统的Session。原因很简单前后端分离后后端要支持无状态接口调用移动端和浏览器端都要能用同一套认证体系。JWT的组成是Header头部、Payload载荷、Signature签名三段后端为用户签发一个包含用户ID、角色、过期时间的加密串前端保存到localStorage每次请求带回来。后端只需要验签不需要在内存里存Session天然支持水平扩展。用户密码加密我推荐用BCrypt这是Spring Security自带的一套密码哈希算法它会在加密时自动附带随机盐即使两个用户密码完全一样存储的密文也不同有效防止彩虹表撞库攻击。千万不要用MD5直接存MD5在2023年之后已经能被GPU集群在很短时间内爆破直接暴露等于裸奔。注册接口的核心逻辑public ResultUserVO register(RegisterDTO dto) { // 1. 校验用户名唯一性 Long count userMapper.selectCount(new LambdaQueryWrapperUser() .eq(User::getUsername, dto.getUsername())); if (count 0) { throw new BusinessException(用户名已存在); } // 2. 密码加密 User user new User(); user.setUsername(dto.getUsername()); user.setPassword(BCrypt.hashpw(dto.getPassword(), BCrypt.gensalt())); user.setRoleId(2); // 默认注册用户是普通消费者角色 user.setStatus(1); // 3. 插入数据库 userMapper.insert(user); return Result.success(convertToVO(user)); }5.2 商品列表的Vue实现与后端接口联调商品列表页是整个商城前端最核心的画面。不要手动写死页面而是用v-for把商品数组渲染出来。每一张商品卡片都是一个独立的Vue组件接收一个product对象作为prop。后端返回的数据要提前约定好结构比如{ code: 200, message: success, data: { total: 128, pageNum: 1, pageSize: 8, list: [ { productId: 101, name: 耐威克全价成猫粮2kg, mainImage: http://127.0.0.1:9000/pet-mall-images/catfood.jpg, price: 88.00, sales: 1024, stock: 456 } ] } }前端拿到这种结构不需要任何处理直接渲染。这就体现了统一返回格式的价值接口的契约非常明确前端照着结构写后端照着结构返联调成本大幅降低。我见过连返回格式都没商量的项目前端拿到的数据一会儿是{data: {...}}一会儿是{result: [...], code: 0}两边改来改去互相怨浪费大量时间。页面上商品卡片要有“加入购物车”按钮这个按钮的事件处理我建议这样拆async handleAddToCart(product) { if (!this.checkLogin()) { this.$router.push(/login) return } try { await this.$store.dispatch(addToCart, { productId: product.productId, quantity: 1 }) this.$message.success(已加入购物车) } catch (error) { this.$message.error(error.message || 加入失败) } }先判断登录状态再调用状态管理派发actionaction内部调用封装好的后端接口返回成功后再用Element UI的Message提示。这个模式下任何组件需要“加入购物车”功能只需要复制这几行代码不用重复写网络请求逻辑了。5.3 购物车与订单流程的完整数据流购物车在Vuex里的数据结构我建议用一个数组套对象cartList: [{ productId, name, price, image, quantity, checked }]。每次操作购物车增删改勾选状态都同步调用后端接口持久化数据同时维护Vuex里的本地数据。不要只存后端也不要只存在本地两者必须同步否则刷新页面状态就丢了。结算按钮的点击逻辑是重头戏前端收集所有checked: true的购物车项组成订单预览数据到结算页结算页提交“创建订单”请求。后端创建订单接口要做四件事校验商品库存、计算总金额不能信前端传的钱必须后端自己算、生成订单号、扣减库存。Transactional public OrderVO createOrder(Long userId, ListCartItemDTO items) { BigDecimal totalAmount BigDecimal.ZERO; ListOrderItem orderItems new ArrayList(); String orderNo generateOrderNo(); for (CartItemDTO item : items) { Product product productMapper.selectById(item.getProductId()); if (product.getStock() item.getQuantity()) { throw new BusinessException(商品「 product.getName() 」库存不足); } totalAmount totalAmount.add(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); // 扣库存UPDATE product SET stock stock - ? WHERE id ? AND stock ? int rows productMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows 0) { throw new BusinessException(商品「 product.getName() 」扣库存失败); } OrderItem orderItem buildOrderItem(orderNo, product, item.getQuantity()); orderItems.add(orderItem); } // 插入订单主表 明细表 Order order buildOrder(userId, orderNo, totalAmount); orderMapper.insert(order); orderItemMapper.batchInsert(orderItems); return convertToVO(order); }这个方法上的Transactional很重要当第三步扣库存抛异常时第一步插入订单主表的操作会跟着一起回滚保证数据库永远处于一致状态。扣库存的SQL要使用“乐观锁”的思路UPDATE product SET stock stock - #{quantity} WHERE product_id #{productId} AND stock #{quantity}通过受影响行数判断库存是否充足避免并发下超卖。6. 常见问题与排查技巧实录6.1 跨域请求与Cookie失效问题后端配置了跨域也报了跨域错十有八九是拦截器处理了OPTIONS预检请求导致的。浏览器在发送真正的POST/DELETE请求前会先发一个OPTIONS预检如果你的拦截器在这个环节就返回401或者设置跨域头后续请求就直接被浏览器拦下了。解决方法是在WebMvcConfigurer里配置跨域的同时拦截器放行OPTIONS请求。Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); }还有登录状态失效的问题。如果你用JWT那么Token存前端localStorage基本不会遇到Session失效但如果代码里混用了Session比如SecurityContextHolder存了Session信息一定要把前端请求的cookie带上否则每次请求都是新用户。我建议项目从第一天就统一用JWT不要Session和JWT混着用混用的排查难度远超想象。6.2 数据库连接、日期时间与时区问题数据库连接串serverTimezoneAsia/Shanghai必须显式写否则MySQL驱动会默认取JVM时区中美标准时间差8小时订单时间、创建时间全部错乱。另外Java实体类里的日期字段用LocalDateTime不要在SQL里依赖数据库的NOW()函数尽量在Java层统一设置否则不同环境的库时间行为可能不一致。分页查询“查出来少了”“数据对不上”这类问题第一反应检查是不是PageHelper.startPage下面跟着两条查询语句。只要是先startPage后跟两条selectPageHelper只对紧邻的那一条生效另一条会用上一次分页线程变量污染。排查时要么拆成两个Service方法要么在startPage之前把ThreadLocal清理掉。6.3 Vue项目常见报错与修复速查我在带项目时总结了一张前端常见问题速查表直接列出来方便大家对照现象原因修复方案npm install一直不成功node_modules缓存损坏或网络源慢删除node_modules和package-lock.json换国内镜像源后重装页面白屏、控制台报[Vue warn]: Property or method not defined模板中用了未定义的变量检查拼写检查data是否完整声明路由跳转后页面不刷新复用组件时生命周期不触发使用watch监听$route参数变化Axios请求返回401拦截不生效响应拦截器里没处理error分支按上文封装方式在error里判断status401商品图片加载404上传成功但URL拼接出错检查Minio的endpoint末尾是否有路径分隔符问题打包后接口404前端打包配置了错误的baseURL将baseURL切换为环境变量或nginx代理路径Element UI的表格组件分页时数据错位也是老生常谈。使用el-table时:data必须是list而不是整个返回对象分页组件的current-change和size-change事件都要正确绑定否则切换每页条数后页码和请求会错乱。6.4 数据库迁移与开发环境同步宠物用品商城支持多角色后经常出现“我本地加了字段同事的库没加”的同步问题。如果你还在靠手工导SQL文件同步表结构我不是针对谁我是说在座各位效率都太低了。我推荐项目从第一天就引入Flyway。Flyway能在Spring Boot启动时自动检测SQL脚本版本按顺序执行未执行的迁移脚本比如V1__init.sql、V2__add_column_to_product.sql。一个人开发可能感受不大但多人协作或者要从毕设答辩环境迁移到部署环境时Flyway能保证环境一一对应不会出现“我本地能跑部署到服务器就打烊”的情况。7. 项目部署上线与性能优化7.1 Spring Boot项目打包jar与war的选择传统的SSM项目部署需要部署到外置Tomcat打包成war包放进webapps目录。Spring Boot默认内置了Tomcat所以打包成可独立运行的jar包是首选。在pom.xml里配置好spring-boot-maven-plugin执行mvn clean package会生成一个带依赖的bootJar。服务器上只需要有JDK环境一条命令启动mvn clean package -DskipTests java -jar pet-mall-server-1.0.0.jar --spring.profiles.activeprod注意两个细节一是要用-DskipTests跳过测试避免测试代码影响打包速度二是生产环境要用--spring.profiles.activeprod指定生产配置让敏感信息数据库密码、Minio密钥从环境变量读取不要把真实密码写在代码里提交到仓库。这是个好习惯哪怕你是个人项目迟早会为这个习惯受益。7.2 Vue项目打包静态资源与API代理前端项目的部署就更简单了。执行npm run buildVue CLI会生成一个dist目录里面的index.html和静态资源直接交给Nginx托管就行。需要注意的问题是打包后的静态资源路径。在vue.config.js里publicPath如果设成./打包后的资源是相对路径部署到Nginx子目录也能用如果设成/是绝对路径部署在域名根目录最省事。很多新手把dist目录直接拖进Tomcat的webapps结果一片空白就是因为这个路径问题。生产环境最稳的做法是前端文件放在Nginx的某个目录API请求统一走/api代理到后端地址前端代码里全部用相对路径。Nginx托管静态文件时对vue的js、css这些资源建议开启gzip压缩响应速度快非常明显gzip on; gzip_min_length 1k; gzip_types text/plain text/css application/javascript application/json image/svgxml;7.3 查询性能与索引优化电商项目上线运行后随着商品数据量增长第一个会响警钟的就是查询变慢。别急着加缓存先把慢SQL日志开起来看对症下药。MySQL慢查询日志开启后定位到执行时间超过1秒的SQL再看执行计划EXPLAIN。常见的优化点有三个商品列表页价格排序、订单列表按用户查询、购物车查询关联商品信息。针对这三个高频场景我分别建了索引ALTER TABLE product ADD INDEX idx_category_price (category_id, price); ALTER TABLE order ADD INDEX idx_user_create (user_id, create_time); ALTER TABLE cart ADD INDEX idx_user_product (user_id, product_id);数据库层优化到一定程度后就要考虑Redis缓存了。商城首页的商品推荐列表、商品分类导航这类变动少、读取多的数据完全可以放进Redis缓存key设计成mall:product:recommend:list过期时间可以设5分钟。这里有个很关键的坑商品上下架或修改价格时缓存必须同步失效否则用户看到的价格是旧的投诉直接飞过来。8. 写在最后我踩过的一些坑这篇文章按我从零搭建一个宠物用品商城的过程把从技术选型到最终部署的主要环节都过了一遍。最后单独说点个人体会比较深的坑供大家参考。第一个大坑是“过早引入分布式”。我见过不止一个做毕设的同学拿着商城项目问“要不要上微服务、用不用消息队列”。如果你是个人开发、或者是毕业设计真没必要。一个单体Spring Boot应用足够支撑几千个用户的商城需求等你真把流量跑大了再拆微服务也不迟。毕设重点是把核心业务做扎实、把文档写完整而不是把架构图搞得天花乱坠。第二个坑在JWT和拦截器的配合。一开始我把JWT校验逻辑写在了Filter里结果文件和拦截器里的逻辑重复导致接口鉴权规则混乱前端一会儿需要登录一会儿不需要。后来统一收敛到HandlerInterceptor里面配合一个个白名单URL路径逻辑才理顺。给权限设置白名单时建议把登录接口、注册接口、商品浏览接口、图片访问接口全部放行只有涉及用户信息、订单、购物车的接口才需要拦截。第三点前端别硬扛样式组件库能用就用。我自己一开始手写过几个商城页面发现日常效率完全比不上直接使用Element UI拖表格、拖表单。用Element UI不代表你会失去个性化能力通过修改主题色和覆盖样式一样能做出视觉统一感强的界面。商城后台管理部分我全程用的Element UI从商品表单到订单表格开发效率比手写CSS快三倍以上。如果这篇文章能帮你在自己的宠物用品商城项目里避开几个坑我就觉得值了。如果你卡在某个具体环节比如Spring Boot启动报错、Vue路由守卫不生效也欢迎在评论区把报错信息贴出来我看到会尽量回复。项目代码永远是自己写一遍才能吃透我的经验只是帮你把路铺平真正走过去的感觉只能靠你自己。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →