ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue宠物商城系统:从数据库设计到前后端联调实战

SpringBoot+Vue宠物商城系统:从数据库设计到前后端联调实战 做Java后端这些年其实被问得最多的一句话就是“能不能给我一套能跑起来的前后端分离项目”问的人里有刚毕业的有转行的还有公司里需要快速搭内部系统的。说实话纯前后端分离、能完整跑通、业务链路又说得清楚的项目市面上大多不是烂尾就是过度包装。这套基于SpringBootVue的宠物商城网站管理系统算是我手里反复打磨过的一套“教科书级”完整案例。技术栈是JavaMySQLMyBatis前端用Vue业务上覆盖了商城最核心的几条链路商品浏览、购物车、下单、支付回调模拟、后台的订单管理和商品管理。这篇文章我完整拆解这个项目的设计与实现包括数据库建模、后端接口设计、前端页面与路由组织、联调部署的细节还有我实操过程中踩过的坑和排查思路。适合正在做毕业设计、准备Java实习面试、或者想上手一套完整全栈项目的同学直接参考。1. 项目整体设计与技术栈选型思路1.1 宠物商城到底在做什么业务宠物商城听起来像是个“卖猫卖狗”的网站但真正落到系统设计上它核心其实是“活体商品销售普通商品销售”混合业态。这跟纯卖数码产品的商城有个非常重要的区别宠物活体不能像普通商品那样放购物车无限下单每一只宠物通常需要独立库存、独立详情页甚至需要单独对接咨询。所以在设计表结构时除了常规的SPU标准化产品单元比如“英短银渐层”和SKU库存量单位比如“3个月大的银渐层母猫”我还是给宠物表单独设计了字段比如宠物年龄、疫苗状态、驱虫状态、是否绝育这些不是普通商品字段但却是宠物交易里用户最关心的信息。系统中我划了“前台用户端”和“后台管理端”两大块前台用户端用户注册登录、宠物商品浏览、分类筛选、关键字搜索、商品详情、加入购物车、提交订单、模拟支付、查看订单状态。后台管理端管理员登录、商品管理上下架、库存调整、价格修改、宠物信息录入、订单审核与发货操作、用户管理、公告发布、数据统计面板。这两块加在一起刚好覆盖了一个“能对外营业”的商城系统需要具备的最基本能力同时每个模块又能独立拆分开来做横向扩展。1.2 为什么选了SpringBootVue而不是其他组合这几年面试和带新人的时候我反复被问到选型的问题。SpringBoot Vue这套组合现在基本是Web全栈开发的“标准答案”但它的合理性不是跟风是有具体逻辑的。后端用SpringBoot核心是它把Spring生态里繁重的XML配置全部自动化了。SpringBoot的starter机制等于把“配置”变成“依赖”比如你想用MyBatis引入一个mybatis-spring-boot-starter再配合application.yml里的数据源配置一个能连数据库的Web服务就起来了。这对毕业设计和中小型团队来说开发效率是成倍提升的。前端用Vue核心原因是它的学习曲线比React平缓而且Vue的双向数据绑定特性特别适合做管理类系统。管理后台大量场景就是“表单绑定数据、数据变更自动刷新视图”Vue的v-model一张表单页能省掉以前用jQuery时需要手动操作DOM的一堆重复代码。另外Vue生态里的Vue Router和Vuex现在更多用Pinia都有非常成熟的约定一个商城的页面路由和登录状态管理用它们很顺手。还有一点这套组合对“查资料”这个动作格外友好。你几乎搜不到一个SpringBootVue的报错是没有前人踩过坑的。对于学生或者刚转行的人来说遇到问题能快速检索到解决方案这本身就是选型时极其重要的隐性成本。2. 系统功能拆解与数据库表结构设计2.1 用户端和管理端的核心功能清单我在设计功能清单时遵循一条原则每个功能必须对应一条业务价值而不是为了凑页面数量。用户端我保留了8个核心模块注册与登录支持用户名密码登录密码不能明文入库用MD5加盐处理生产环境建议BCrypt当前项目为了演示直观保留了MD5工具类。首页轮播图展示后台可配置轮播图前端首页接口动态读取。商品分类导航两级分类比如“宠物类-猫咪-英短”“宠物用品-猫粮-幼猫粮”。商品搜索支持按商品名称模糊查询底层是MyBatis的LIKE动态SQL拼接。商品详情页展示图文信息、库存、销量宠物类型还额外展示年龄、疫苗等属性。购物车管理用户添加商品到购物车、调整数量、删除、选中结算。订单提交与支付模拟订单提交后生成订单号模拟支付按钮直接更新订单状态。个人中心我的订单、收货地址维护、个人信息修改。管理端相对精简但后台才是这个系统真正的“管理”含义所在管理员登录控制独立的登录入口校验管理员角色。商品管理新增、修改、下架、删除商品支持图片上传。宠物管理单独管理宠物类商品维护年龄、疫苗、驱虫、售价、库存。订单管理查看所有订单订单状态流转待付款、待发货、已发货、已完成、已取消。用户管理查询用户列表禁用/启用用户账号。公告管理发布系统公告前台首页展示。数据统计后端提供简单统计接口前端用图表展示商品数和订单销售额。2.2 数据库表结构6张核心表的设计逻辑这套系统的数据库我一共设计了6张核心表外加1张管理员表。表结构是整套项目的地基地基歪了后面全是坑。我按表逐个讲清楚设计意图用户表user字段名类型说明idbigint主键自增usernamevarchar登录用户名唯一索引passwordvarchar密码MD5加密存储nicknamevarchar昵称phonevarchar手机号avatarvarchar头像URLstatustinyint1正常0禁用create_timedatetime注册时间这条表结构看起来简单但有个设计细节用户名我加了唯一索引。这个索引是数据库层面的兜底防止并发注册下出现重复用户名。商品分类表category字段名类型说明idbigint主键namevarchar分类名称parent_idbigint父分类ID0为顶级分类sortint排序权重两级分类用parent_id自关联表结构简单而且查询时先查顶级再按父ID查子级配合MyBatis可以轻松封装成父子结构的JSON返回给前端。商品表product字段名类型说明idbigint主键namevarchar商品名称category_idbigint所属分类IDpricedecimal(10,2)售价stockint库存salesint销量main_imagevarchar主图URLdetail_imagestext详情图URLJSON数组字符串statustinyint1上架0下架descriptiontext商品描述商品表我没有把宠物和普通商品分成两张表而是采用“一张商品表一个商品类型字段”的方式。因为宠物和普通商品90%的字段是重叠的拆分反而让查询变复杂。但宠物特有的属性怎么办我在后面加了个product_attribute表以键值对的形式存扩展属性例如“keyage, value3个月”。这种设计叫EAV模型实体-属性-值在电商系统里非常常见它最大的好处是扩展字段不需要改表结构。购物车表cart字段名类型说明idbigint主键user_idbigint用户IDproduct_idbigint商品IDquantityint数量checkedtinyint是否选中1选中0未选中create_timedatetime加入时间购物车表看起来最简单但有一个关键约束user_id product_id必须做成唯一索引。不然用户手速快连续点两次“加入购物车”表里就出现两条同商品记录这是典型的脏数据。订单表orders和订单明细表order_item订单表我把核心字段列出来字段名类型说明idbigint主键order_novarchar订单编号全局唯一user_idbigint用户IDtotal_pricedecimal(10,2)订单总金额statustinyint0待付款1待发货2已发货3已完成4已取消receiver_namevarchar收货人姓名receiver_phonevarchar收货人电话receiver_addressvarchar收货地址create_timedatetime下单时间pay_timedatetime支付时间订单明细表字段名类型说明idbigint主键order_idbigint订单ID关联orders.idproduct_idbigint商品IDproduct_namevarchar商品名称快照product_imagevarchar商品图片快照pricedecimal(10,2)成交单价quantityint购买数量订单为什么会有明细表因为订单和商品是“一对多”关系一个订单可以买多个商品。还有一点是我不允许订单明细表直接关联商品表去JOIN查商品名称。因为商品信息是会变的——名称可能被管理员改、商品可能被删除、价格会调整。订单是交易快照落库的时候就必须把商品名称、图片、价格原样拷贝一份不然以后用户查历史订单会看到产品的当前信息这在交易系统里是大忌。管理员表admin字段名类型说明idbigint主键usernamevarchar管理员用户名passwordvarchar密码rolevarchar角色admin/operator后台权限我用一个role字段搞定没有引入Spring Security和复杂的RBAC因为这个项目要演示的核心是商城链路权限体系做到“能登录、能区分角色”即可。引入大量安全框架反而会让代码的可读性下降初学者拿到源码会迷失在过滤器链里。3. 后端核心实现SpringBootMyBatis的落地细节3.1 Maven工程结构与依赖管理后端工程我按标准的Maven结构组织包结构如下com.petmall ├── PetMallApplication.java // 启动类 ├── controller/ // 前端控制器层 │ ├── UserController.java │ ├── ProductController.java │ ├── CartController.java │ ├── OrderController.java │ └── AdminController.java ├── service/ // 业务逻辑层 │ ├── UserService.java │ ├── ProductService.java │ ├── CartService.java │ ├── OrderService.java │ └── impl/ // 实现类 ├── mapper/ // MyBatis Mapper接口 │ ├── UserMapper.java │ ├── ProductMapper.java │ ├── CartMapper.java │ └── OrderMapper.java ├── entity/ // 实体类 ├── dto/ // 数据传输对象接收前端请求参数 ├── vo/ // 视图对象返回给前端的数据结构 ├── common/ // 通用类统一返回结果、异常处理、工具类 └── config/ // 配置类拦截器、跨域配置等pom.xml里关键的依赖项有parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependencies这里要特别提醒mybatis-spring-boot-starter的版本和SpringBoot版本是有关联的。比如SpringBoot 3.x搭配MyBatis starter 3.0SpringBoot 2.x搭配MyBatis starter 2.3.x。很多同学在项目启动时报Invalid value type for attribute factoryBeanObjectType或者各种奇怪的Bean创建异常八成就是版本匹配问题。我的建议是直接用2.7.18配2.3.1这个组合我线上跑了大半年稳得很。3.2 YAML配置与MyBatis映射规则application.yml是整个后端配置的核心。下面是我实际使用的配置server: port: 8080 servlet: context-path: /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pet_mall?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.petmall.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl重点讲三个容易被忽略的细节第一map-underscore-to-camel-case: true。这项配置让数据库字段create_time自动映射到Java实体类的createTime属性。不开启的话你查询出来的时间字段全是null还找不到原因。我当时排查一个很诡异的问题查了半小时结果就是忘了开驼峰映射。第二log-impl: org.apache.ibatis.logging.stdout.StdOutImpl。开发阶段把MyBatis的SQL日志直接输出到控制台效果是你可以实时看到每一条SQL语句和传入的参数值。这是调试SQL的利器排查“数据库里能查出来但接口返回null”这一类问题时帮了大忙。部署上线前记得把这项关掉不然日志里全是SQL刷屏。第三连接串末尾的allowPublicKeyRetrievaltrue。MySQL 8.x的驱动和MySQL 5.7不一样本地密码登录时如果不开这个参数会报Public Key Retrieval is not allowed。这个报错太容易被新手忽略了其实是驱动版本和服务器认证方式的安全机制导致的。3.3 Mapper接口与XML实现一个商品列表查询的完整示例数据访问层我用的是MyBatis的XML方式写SQL开发期直观后期要调优也能精准修改。商品列表的分页查询是商城系统使用频率最高的接口之一我以它为例说明整个链路。Mapper接口Mapper public interface ProductMapper { ListProductVO selectProductList(Param(keyword) String keyword, Param(categoryId) Long categoryId, Param(offset) int offset, Param(limit) int limit); int countProduct(Param(keyword) String keyword, Param(categoryId) Long categoryId); Product selectById(Param(id) Long id); }对应的XML文件mapper namespacecom.petmall.mapper.ProductMapper select idselectProductList resultTypecom.petmall.vo.ProductVO SELECT p.id, p.name, p.price, p.stock, p.sales, p.main_image, p.status, c.name AS category_name FROM product p LEFT JOIN category c ON p.category_id c.id where if testkeyword ! null and keyword ! AND p.name LIKE CONCAT(%, #{keyword}, %) /if if testcategoryId ! null AND p.category_id #{categoryId} /if AND p.status 1 /where ORDER BY p.sales DESC, p.id DESC LIMIT #{offset}, #{limit} /select select idcountProduct resultTypeint SELECT COUNT(*) FROM product p where if testkeyword ! null and keyword ! AND p.name LIKE CONCAT(%, #{keyword}, %) /if if testcategoryId ! null AND p.category_id #{categoryId} /if AND p.status 1 /where /select /mapper这个例子包含了几个非常实用的MyBatis知识点动态SQL的where标签的妙处是它自动处理“第一个条件前面的AND”。你写AND p.name LIKE...如果前面没有其他条件where会自动把这个AND去掉。但如果你不写where而是手动写WHERE 11也能跑只是不够优雅。虽然11在数据量不大时性能影响微乎其微但面试聊到SQL优化时会觉得很不专业所以我还是用where。分页参数直接用LIMIT。原来商城系统数据量小没必要上PageHelper插件。PageHelper虽然方便但偶尔会有“线程复用导致分页参数串味”的坑——就是你一个接口没写分页但查询结果却被截断成10条这种莫名其妙的问题遍布全网排查起来特别浪费时间。直接用LIMIT #{offset}, #{limit}分页逻辑由自己控制清清楚楚。#{keyword}用#{}预编译占位防SQL注入。千万别用${keyword}直接拼接这个操作在生产环境等于把你的数据库裸奔。#{}最终会被MyBatis替换成?占位符交给JDBC的PreparedStatement去参数化执行。3.4 业务层的设计下单接口的事务控制业务层是设计的核心所在尤其下单接口一不留神就能搞出超卖或者库存扣成负数。我先讲一下事务控制的完整逻辑。OrderService.createOrder()里我做了这样一串操作校验用户登录状态根据cartIds查出购物车中选中的商品列表逐个商品校验库存是否足够循环扣减库存UPDATE product SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity})生成订单主表和订单明细清空对应的购物车记录。这6步必须保证“要么全部成功要么全部失败”所以我在方法上加Transactional注解。这里有一个很重要的经验库存扣减不能用“先查库存再UPDATE”两步走。并发环境下两个请求同时查到了库存为1都认为可以卖然后都执行UPDATE库存变成-1超卖就是这么来的。正确做法是直接在UPDATE语句里带上stock #{quantity}条件用数据库行锁来保证原子性如果受影响行数为0说明库存不足直接抛异常回滚事务。这种扣库存写法是电商项目里最基础的防超卖方案也是面试热门考点大家一定要记住。Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, ListLong cartIds, AddressVO address) { // 1. 查询购物车信息 ListCartItemVO cartItems cartMapper.selectCartItemsByIds(cartIds); if (cartItems.isEmpty()) { throw new BusinessException(购物车没有选中商品); } // 2. 计算总价 扣减库存 BigDecimal totalPrice BigDecimal.ZERO; for (CartItemVO item : cartItems) { int rows productMapper.reduceStock(item.getProductId(), item.getQuantity()); if (rows 0) { throw new BusinessException(商品[ item.getProductName() ]库存不足); } totalPrice totalPrice.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 3. 插入订单主表 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalPrice(totalPrice); order.setStatus(0); // ... 设置收货信息 orderMapper.insert(order); // 4. 插入订单明细 orderItems.forEach(item - { item.setOrderId(order.getId()); orderItemMapper.insert(item); }); // 5. 清空购物车已购买项 cartMapper.deleteByIds(cartIds); return order.getId(); }订单号的生成也有讲究我没用数据库自增ID充订单号那样会暴露系统销量和规律。我用的是yyyyMMddHHmmss 4位随机数 用户ID后4位拼成24位订单号虽然本身不保证100%唯一但配合数据库唯一索引如果插入时冲突重试一次就能解决。这么组合的好处是从订单号可以直接看出下单时间和用户维度排查问题时特别方便。3.5 MyBatis一级缓存与二级缓存的实际处理热搜词里有“mybatis缓存”我多说一嘴。MyBatis有一级缓存SqlSession级别和二级缓存Mapper级别。默认情况下一级缓存是开启的二级缓存默认关闭。在实际项目中我对二级缓存的态度是商城这种高频读写的系统别开二级缓存。因为缓存失效策略在商品价格、库存这类字段上极难把握一个商品价格变更了缓存没刷新用户下单就是错误的金额这会出大事。加上项目接入了Redis做热点数据的缓存之后MyBatis二级缓存就显得鸡肋了。不过一级缓存有个隐藏的坑值得大家留意在同一个SqlSession里执行两次相同的查询第二次走缓存不会命中数据库。听起来没什么但如果你在一个事务方法里先查询商品再在别的服务里更新了商品其实是同一个SqlSession打开的第二次查询拿到的还是旧数据。排查这类问题最耗时间我的经验是对数据一致性要求高的查询直接在Mapper XML里设置useCachefalse或者flushCachetrue。4. 前端核心实现Vue工程搭建与商城页面开发4.1 Vue环境的搭建与工程化准备前端这块我用的是Vue 2生态。虽然Vue 3 Vite是当前新项目的首选但考虑到这套源码要配合SpringBoot 2.7使用而且网上现有组件库、教程、踩坑帖子最丰富的仍是Vue 2 ElementUI所以我最终为项目选了Vue 2稳定路线。Node.js环境建议装14.x或者16.x LTS版本别用太新的Node 20有些旧版依赖在编译时可能会出兼容问题。创建一个Vue项目的标准流程是npm install -g vue/cli vue create pet-mall-web创建过程中选择Manually select features勾选Router、VuexCSS预处理器选择SCSS。这个交互式配置完了工程目录就是标准化的src ├── main.js // 入口文件 ├── App.vue // 根组件 ├── router/ // 路由配置 ├── store/ // Vuex状态管理 ├── api/ // axios接口封装 ├── views/ // 页面组件 │ ├── user/ // 用户端页面 │ ├── admin/ // 管理端页面 │ └── common/ // 公共页面登录、注册、403等 ├── components/ // 可复用组件 └── utils/ // 工具函数axios实例、token存储等工程搭好后紧接着装基础依赖npm install element-ui axios vuex vue-router3 sass-loader10 node-sass6这里有两个关键版本坑我如实说一下第一vue-router3是Vue 2专用的到了Vue 3要用vue-router4两套API差异很大比如createRouter取代了new Router()下载错版本页面会白屏。第二sass-loader必须用10.x版本配node-sass6.x如果用默认装的sass-loader13大概率直接报TypeError: this.getOptions is not a function。我在这个报错上帮人排查的次数太多了大家直接锁定版本省心。4.2 前端路由设计与权限控制商城的路由分成“用户端”和“管理端”两块我采用嵌套路由结构const routes [ { path: /, component: Layout, children: [ { path: , name: Home, component: () import(/views/user/Home.vue) }, { path: product/:id, name: ProductDetail, component: () import(/views/user/ProductDetail.vue) }, { path: cart, name: Cart, component: () import(/views/user/Cart.vue) }, { path: checkout, name: Checkout, component: () import(/views/user/Checkout.vue) }, { path: orders, name: Orders, component: () import(/views/user/Orders.vue) } ] }, { path: /admin, component: AdminLayout, children: [ { path: , name: AdminDashboard, component: () import(/views/admin/Dashboard.vue) }, { path: products, name: AdminProducts, component: () import(/views/admin/ProductManage.vue) }, { path: orders, name: AdminOrders, component: () import(/views/admin/OrderManage.vue) } ] }, { path: /login, component: () import(/views/common/Login.vue) }, { path: /register, component: () import(/views/common/Register.vue) } ];路由层面我加了全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path.startsWith(/admin) !token) { next(/login); return; } next(); });这个守卫的逻辑不复杂但它是前端权限的第一道门槛。管理端页面在后端接口上还会二次校验管理员身份双保险。需要注意一点组件我用的是() import()懒加载写法目的是让首屏只加载首页相关的代码路由切换时才动态加载对应组件这样能显著缩短首页白屏时间。打包后的代码会被自动拆分成多个chunk配合nginx的gzip压缩整个项目体量控制得很理想。4.3 Axios封装与Vuex状态管理axios实例的封装是前后端对接的桥梁我统一在utils/request.js里处理import axios from axios; import { Message } from element-ui; import router from /router; const service axios.create({ baseURL: /api, timeout: 10000 }); // 请求拦截器统一携带token service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] token; } return config; }); // 响应拦截器统一处理业务码和异常 service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { Message.error(res.msg || 系统异常); if (res.code 401) { localStorage.removeItem(token); router.push(/login); } return Promise.reject(new Error(res.msg)); } return res.data; }, error { Message.error(error.response?.data?.msg || 网络异常); return Promise.reject(error); } ); export default service;这份封装解决了两个高频问题所有请求自动带token省得每个业务接口手动加header后端返回的code/msg/data统一结构被剥离页面代码只关心数据本身不需要每个页面写一套判空和报错逻辑。Vuex里我维护了一个user模块保存用户信息和登录状态const userStore { state: { token: localStorage.getItem(token) || , userInfo: JSON.parse(localStorage.getItem(userInfo) || {}) }, mutations: { SET_TOKEN(state, token) { state.token token; localStorage.setItem(token, token); }, SET_USER_INFO(state, info) { state.userInfo info; localStorage.setItem(userInfo, JSON.stringify(info)); }, LOGOUT(state) { state.token ; state.userInfo {}; localStorage.removeItem(token); localStorage.removeItem(userInfo); localStorage.removeItem(cartCount); } } };为什么不把用户信息只存在内存里因为浏览器刷新一次Vue实例重新创建内存中的数据就全没了。所以token和用户基本信息同步写入localStorage刷新后依然能恢复登录状态。token过期由后端返回401来通知前端清理这是我目前认为最直白且容错高的方案。4.4 商品列表页与购物车页的实现要点商品列表页是整个商城前台流量最大的页面我做了商品卡片栅格布局每行4个商品卡片包含主图、价格、销量等核心信息。关键点是图片懒加载ElementUI的el-image自带lazy属性加上它之后首屏会少发起很多图片请求页面打开速度立刻不一样。商品详情页的“加入购物车”操作前端代码很简单async function addToCart() { const res await addCart(product.id, quantity); cartCount.value await getCartCount(); }但这里有个容易忽略的细节加入购物车成功后顶部导航栏的购物车角标数量需要同步刷新。我用的是Vuex中维护一个cartCount在main.js里调用一次getCartCount初始化然后每次加购后手动更新。为什么不每次进页面都实时查一次因为频繁请求会增加后端压力而且数量变化最频繁的就是加购这个动作维护一个全局计数就够了。购物车页我用el-table展示已选商品每行有数量增减器。前端在点击“结算”时需要把选中的购物车行id列表传给后端const selectedIds selectedRows.map(row row.id); const res await createOrder(selectedIds, address);这里用到的是购物车行的id而不是商品id因为同一个用户可能在购物车里有同一商品的多个条目。用购物车id删除和创建订单才是准确的操作。5. 前后端联调与打包部署实战5.1 开发期的跨域问题与Vue代理配置“前后端分离”这句话说得容易开发期第一个迎面而来的关卡就是跨域。前端的域名是http://localhost:8081后端的接口是http://localhost:8080/api浏览器默认会拦截这种跨源请求。我推荐开发期用Vue CLI的devServer代理来解决改vue.config.jsmodule.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } };这样配置后前端请求/api/product/list会被devServer自动转发到http://localhost:8080/api/product/list。浏览器角度看到的是请求同源地址跨域问题就烟消云散了。生产环境部署时再用Nginx配置反向代理把/api指向后端保持前端代码零改动。如果后端确实需要开启CORS我通常是在SpringBoot里加一个WebMvcConfigurer的配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600); } }但要注意如果前端走了代理后端就不需要再配CORS两者选其一即可。两套同时配反而可能出一些怪问题比如预检请求过了但实际请求带了自定义header被拦排查起来挺费劲。5.2 前端打包后的静态资源如何交给SpringBoot托管开发完成后部署方案通常有两种前端独立部署到Nginx或者把前端打包产物放进SpringBoot的static目录一个Java进程搞定全部。我后面这种方案对个人项目和毕业设计最省事服务器只需要装JDK和MySQL不用额外整Nginx成本低、部署链路短。操作流程是npm run build打包完成后dist目录下会生成index.html和static/js、static/css等静态资源。把这些文件复制到后端项目的src/main/resources/static目录下重新打包SpringBoot的jar包启动后访问http://localhost:8080就能看到商城前台。但这里有一个坑Vue默认打包后的静态资源路径是绝对路径/js/app.js它会直接找服务器根目录下的js目录。放在SpringBoot的static下时由于后端配置了server.servlet.context-path: /api根路径/并不直接是static目录的映射地址这会导致资源404。解决办法有两种在vue.config.js里设置publicPath: ./让打包后的资源引用变成相对路径。后端不做context-path配置或者用Nginx映射。我在项目里直接把后端的context-path去掉同时前端baseURL改成完整路径http://localhost:8080/api然后在部署时用Nginx把/api反代到后端。这个方案线上最稳而且以后前后端要拆分部署也不需要改代码。5.3 常见问题与排查技巧实录做这套系统过程中我整理了以下几个典型问题的排查思路基本都是大家未来一定会踩到的问题一启动报错Failed to configure a DataSource: url attribute is not specified原因SpringBoot自动装配尝试创建数据源但application.yml里没有配置数据源或配置文件没被扫描到。排查顺序检查application.yml是否在src/main/resources下检查是否拼写正确是spring.datasource.url不是spring.dataSource.url检查是否引入了错误的依赖比如你不小心引了spring-boot-starter-jdbc但没配数据源。问题二MyBatis查询返回的数据全是null原因90%是驼峰映射没有开或是实体类属性名与数据库列名不对应。检查mybatis.configuration.map-underscore-to-camel-casetrue是否生效。另外resultType写成了实体类但SQL里用了AS别名MyBatis无法反解时也会返回null。问题三前端页面白屏控制台报Cannot read properties of undefined这个通常是接口数据层级和页面预期不符。比如后端返回{code:200, data:{list:[]}}但你的页面代码直接用了res.list此时res实际上是data对象应该写res.list而不是res.data.list。我的建议是页面里所有接口数据都打一次console.log(res)看结构再动手写模板绑定。问题四跨域接口通了但带Token的请求报401因为浏览器先发一个OPTIONS预检请求这个请求默认没有携带Authorization头。如果你在后端拦截器里对所有请求都要求登录校验预检请求就会被拦截。解决办法在拦截器中放行OPTIONS请求或者配置CORS时让Spring的拦截器不对OPTIONS做鉴权。拦截器放行写法if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; }问题五Vue打包后部署刷新404这是前端路由的经典问题。vue-router默认是Hash模式URL形如/#/product/1刷新不会出问题。如果用了History模式URL是真实的路径/product/1刷新时后端没有这个路径的映射就会404。解决思路部署环境配Nginx时加try_files $uri $uri/ /index.html;。如果你不想为这个搞Nginx最简单就是用Hash模式前端代码改一行const router new VueRouter({ mode: hash, routes });5.4 系统后续可以怎么扩展这套项目的边界很清楚它是一个单机版、无中间件依赖的商城系统。生产级的商城还缺不少东西但作为一套完整源码和毕业设计它的扩展方向反而很值得说接入Redis做商品详情和分类的缓存扛住更高的并发读。引入Spring Security或Sa-Token做更细粒度的权限控制比如管理员的操作审计日志。订单模块加上真正对接微信支付/支付宝支付的流程而不是模拟支付。引入消息队列RocketMQ或RabbitMQ处理下单后发送短信通知、库存异步扣减等场景。加一个ElasticSearch商品检索引擎替代MySQL的LIKE模糊查询搜索体验完全不是一个档次。6. 个人经验总结我最后聊一点实际的体会。这套项目我在带新人、指导学生做毕设的过程中重复推演过很多次最大的感受是技术栈新旧不是关键链路完整才是关键。很多同学做一个系统页面做了一大堆但下单超卖、库存为负、订单状态乱跳这类核心业务问题根本没法解释那这个项目就是“看起来能用经不起问”。反过来如果能把商品、购物车、订单、库存这四条链路在设计上有理有据地讲清楚哪怕技术是SpringBootJSP都照样能打动面试官。你要真正用好这个项目不建议只盯着源码看。我建议按这个顺序动手先跟着文档把数据库建好然后启动后端用Postman把商品列表、加入购物车、下单接口挨个调通再启动前端把页面跑起来最后自己从头写一遍订单模块的代码把它变成你自己的东西。源码是参考书不是你复制粘贴的草稿。等你哪天下单、扣库存、事务回滚这套逻辑能不看代码自己画出来你对Java后端开发的基本盘就有底了。
返回列表