宠物用品交易网站听起来是个很“传统”的练手项目,但把商品、购物车、订单、用户、后台管理这一整套流程用 SpringBoot + Vue + MyBatis + MySQL 跑通,你会发现里面全是前后端分离项目实战的经典知识点。这个项目我前后搭了三遍,第一遍败在版本搭配,第二遍死在联调跨域,第三遍才把整套体系摸顺。所以这篇文章我把完整源码和部署教程结合起来写,从架构设计、环境准备、模块实现到上线部署,每一步都讲清楚“为什么这么做”。适合正在准备毕业设计、想系统入门前后端分离实战,或者想拿一套完整电商项目源码快速学习的人直接参考。
1. 项目整体架构与设计思路
1.1 为什么选前后端分离架构
我见过很多同学做交易网站用 JSP + Servlet,页面和服务端代码强耦合。改个按钮样式要重启 Tomcat,管理员和买家页面全挤在一套模板里,越改越乱。前后端分离的核心思路是把“数据”和“展示”彻底拆开:后端只暴露接口返回 JSON,前端用 Vue 接收数据自己渲染 DOM。这样买家端的商品列表、购物车、订单页面,和管理员端的商品管理页,本质上都是同一个后端接口池里取数据,各自独立维护。
这个宠物用品交易系统的业务流程本身也天然适合接口化。用户注册登录、首页轮播图拉取、商品分类分页查询、加购下单个个都是标准的 REST 接口:POST /user/login、GET /product/list、POST /cart/add、POST /order/create。每个接口只干一件事,前端拿到数据后自行处理状态和交互。团队协作层面也更舒服——前端和后端可以并行开发,只要先把接口文档约定好,两边各写各的,最后联调时集中解决跨域和字段对齐问题。
1.2 技术栈选型背后的“为什么不”
这个项目的后端我选了 SpringBoot + MyBatis,前端选 Vue,数据库用 MySQL。这套组合在 Java 岗位招聘要求里出现频率非常高,可以说是“求职标配”。但选型时我慎重做了几个判断。
第一,Spring Boot 版本一定要克制。网上很多人直接下了最新版 Spring Boot 3.x,结果 MyBatis 官方 starter 还没跟上、或者 JDK 必须升到 17、又或者 druid 连接池配置方式变了,一堆兼容问题冒出来——这就是“springboot版本太高”这类搜索热的由来。我这里用的是 Spring Boot 2.7.18 + JDK 8,成熟稳定,MyBatis、druid、lombok 全都能平滑兼容。如果你的机器已经装了 JDK 17,也可以跑 2.7.x,但建议统一 JDK 8,省得后面部署时环境不一致。
第二,持久层为什么选 MyBatis 而不是 Spring Data JPA?宠物用品交易涉及多表联查的场景很多,比如查订单时需要 join 商品表和订单明细表,MyBatis 可以直接写 SQL,SQL 怎么写完全由我控制,调优也直观。JPA 的自动生成 SQL 虽然写着省事,但一旦出现复杂查询,生成的 SQL 性能不可预知,排查问题还要先翻译 JPQL,对新手并不友好。MyBatis 的 mapper 接口 + XML 映射文件虽然多写几行,但每个 SQL 都明明白白。
第三,Vue 前端我用的是 Vue 3 + Vue Router + Pinia。Vue 3 的 Composition API 写业务逻辑时组织性更强,Pinia 比 Vuex 少很多样板代码。如果你以前只学过 Vue 2,也不用慌,本文的组件代码大部分在 Vue 2 里改改写法也能跑。
1.3 功能模块划分与数据库表设计
先拆功能,再建表,这是一定要遵守的顺序。站在买家角度,系统要有:注册登录、浏览商品、按分类搜索、查看商品详情、加入购物车、提交订单、查看我的订单。站在管理员角度,系统要有:商品分类管理、商品管理(上架/下架/编辑)、订单状态管理、首页轮播图管理。
基于这些功能,我设计了 8 张核心表,如下表所示:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| user | 用户表 | id, username, password, phone, avatar |
| admin | 管理员表 | id, username, password |
| category | 商品分类表 | id, name, sort |
| product | 商品表 | id, category_id, name, subtitle, price, stock, main_image, status |
| cart | 购物车表 | id, user_id, product_id, quantity, checked |
| orders | 订单主表 | id, order_no, user_id, total_price, status, create_time |
| order_item | 订单明细表 | id, order_id, product_id, product_name, product_image, price, quantity |
| banner | 轮播图表 | id, image_url, link_url, sort |
这里有几个设计要点,我在建表时反复斟酌过:订单表一定要拆主表和明细表。如果一个订单包含 3 种商品,只有明细表一字排开,才能准确记录每种商品的快照价格和数量,而不是只在主表里存一个总价。product 表的 status 字段用于软上下架,不要真的删除商品记录,否则历史订单明细就变成“无源之水”。用户密码不要明文存,后面我会讲用 BCrypt 加密。
2. 环境准备与项目初始化
2.1 JDK、Maven、MySQL 的版本搭配与安装
先把本地环境准备好。我的建议配置是 JDK 1.8(8u202 以上)、Maven 3.8.x、MySQL 5.7.44,开发工具用 IntelliJ IDEA 2022+。JDK 安装完后,记得配好环境变量 JAVA_HOME 和 PATH。注意不要只配 PATH 不配 JAVA_HOME,很多 IDE 和 Maven 启动脚本是靠 JAVA_HOME 来找 JDK 的。Maven 安装后,在 conf/settings.xml 里加一个阿里云镜像,否则拉依赖就像蜗牛爬:
<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <url>https://maven.aliyun.com/repository/public</url> </mirror>MySQL 安装是很多人头疼的点。Windows 10 上装 MySQL 5.7.44 时,我总结了一套最省心的流程:先下载 mysql-5.7.44-winx64.zip 免安装版,解压到 D:/mysql。然后在 D:/mysql 下新建 my.ini,配置基于目录和端口:
[mysqld] basedir=D:/mysql datadir=D:/mysql/data port=3306 character-set-server=utf8mb4以管理员身份打开 cmd,进入 bin 目录后依次执行mysqld --initialize-insecure、mysqld install、net start mysql。初始化用--initialize-insecure表示 root 密码为空,启动成功后用mysql -u root -p登录,再执行ALTER USER 'root'@'localhost' IDENTIFIED BY '你的密码';设置密码。整套流程大概 5 分钟,比安装版省心得多,卸载也干净。
Linux 服务器上我部署的是 MySQL 8.0。注意 8.0 默认认证插件是 caching_sha2_password,项目里的 JDBC 连接串必须带上allowPublicKeyRetrieval=true,否则会报 Public Key Retrieval is not allowed。这个坑我部署时遇到过,印象很深。
2.2 Vue 环境配置与脚手架项目创建
前端环境要求 Node.js 16+ 和 npm。建议先换 npm 镜像源,执行npm config set registry https://registry.npmmirror.com。然后全局安装 Vue CLI:npm install -g @vue/cli,验证版本用vue --version。创建项目时执行vue create pet-front,选择 Vue 3 预设。这个创建过程要等一段时间,如果你发现卡在 node-sass 之类的位置半天不动,大概率是网络问题,重新切镜像后再试。
项目创建完成后,继续安装路由、状态管理和 HTTP 库:npm install vue-router@4 pinia axios。这里我想专门提一下 Vue Router。系统里路由分两块:买家端路由(/home、/product/list、/cart、/order),管理员端路由(/admin/dashboard、/admin/goods)。最好在路由配置里加一个前置守卫:访问购物车、订单等页面时判断本地是否有 token,没有则跳转登录页。这个“路由守卫 + token 校验”是前后端分离项目里权限控制最常见的实现方式。
2.3 后端工程结构初始化与基础配置
后端的 Maven 工程我命名为 pet-server,采用标准分包结构:
com.pet.server ├── common // 统一返回、异常处理、常量 ├── config // 拦截器、跨域配置 ├── controller // 接口层 ├── entity // 数据库实体 ├── mapper // MyBatis 接口 ├── service // 业务逻辑层 └── utils // 工具类(JWT、MD5等)这种分层的目的很明确:controller 只负责接收参数和返回结果,不写业务逻辑;service 处理业务规则,比如下单时要校验库存、计算总价;mapper 只做数据读写。我见过很多项目把所有代码堆在 controller 里,一个方法几百行,后面根本没法维护。
application.yml 里核心配置如下:
server: port: 8088 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pet_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 type: com.alibaba.druid.pool.DruidDataSource mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.pet.server.entity configuration: map-underscore-to-camel-case: true特别注意map-underscore-to-camel-case: true。数据库字段是下划线风格(如 create_time),Java 属性是驼峰风格(createTime),打开这个配置后 MyBatis 会自动映射,省去大量 ResultMap 手动映射的样板代码。
3. 核心业务模块实现
3.1 用户注册、登录与 JWT 认证
用户模块是第一个要写的功能,因为购物车、订单都依赖用户身份。注册接口的逻辑:前端传 username 和 password,后端先查重,再对密码做 BCrypt 加密,然后插入 user 表。BCrypt 是单项 hash 算法,每次生成的密文都不同,比 MD5 安全得多,即使数据库被拖库,也无法通过彩虹表反推出原始密码。
登录接口是重点。我采用了 JWT(JSON Web Token)做无状态认证——后端登录成功时生成一个 token 返回给前端,前端把 token 存到 localStorage,之后每次请求在请求头带Authorization: Bearer token。后端写一个拦截器统一校验 token,解析成功就放行,否则返回 401。JWT 的优势很明显:服务器不保存 session,天然适合前后端分离。服务器部署多实例时,session 还需要 Redis 做共享,JWT 完全不需要。我用的是 jjwt 0.9.1 这个库,生成 token 的代码大致是这样:
String token = Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim("username", user.getUsername()) .setExpiration(new Date(System.currentTimeMillis() + 7*24*3600*1000L)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();token 有效期我设置的是 7 天,这样用户在移动端和 Web 端体验都比较舒服。密钥 secretKey 不要写在代码里,从配置文件中读取,避免泄露。拦截器里需要对登录和注册两个接口做白名单放行,其他接口都过校验。前端路由守卫和后端拦截器的双重校验,保证了即使有人绕过前端直接调接口,没有 token 也拿不到数据。
3.2 商品模块与前端商品卡片组件
商品模块是整个系统的门面。后端接口有三个:分类列表、分类下商品分页查询、商品详情。分页查询这里我用 MyBatis 手动写分页 SQL,没有引入 PageHelper 插件,原因是这个项目的表数据量并不大,LIMIT 直接干就完了,少一个依赖少一个坑。核心 SQL 如下:
<select id="selectPageByCategoryId" resultType="Product"> SELECT p.id, p.name, p.subtitle, p.price, p.main_image, p.stock FROM product p WHERE p.status = 1 <if test="categoryId != null"> AND p.category_id = #{categoryId} </if> ORDER BY p.id DESC LIMIT #{offset}, #{pageSize} </select>前端做一个商品卡片组件 ProductCard.vue,用 Vue 3 的 slot(插槽)来扩展:卡片底部预留一个操作插槽,买家页放“加入购物车”按钮,管理员页放“编辑/上下架”按钮。同一个组件两种用法,这就是插槽的典型价值——不需要复制粘贴第二套卡片代码:
<template> <div class="product-card"> <img :src="product.mainImage" :alt="product.name" /> <div class="product-info"> <h4>{{ product.name }}</h4> <p class="price">¥{{ product.price }}</p> <slot name="actions"></slot> </div> </div> </template>买家页面调用时传入购物车按钮,管理员页面传入编辑按钮,组件内部零改动。Vue 插槽这个功能很多人写组件时容易忽略,但这恰恰是组件复用性高低的关键。
3.3 购物车与订单模块实现
购物车表是典型的“中间关系表”,以 user_id + product_id 定位一条记录。加入购物车接口的逻辑要注意幂等性:同一用户加同一个商品时,如果购物车已有记录,就 update quantity(数量+1),而不是 insert 一条新记录。否则用户手滑点了两次加购,购物车里会出现两行同一个商品。
下单流程是整个系统业务最重的部分,我详细说一下。前端从购物车勾选记录提交订单,传一个 cartIds 数组;后端查询这些购物车记录,join 出商品实时价格和库存;然后循环校验库存是否充足,只要有一个商品库存不足,整个下单事务回滚;接着扣减库存,计算总价,插入订单主表和明细表;最后删除对应的购物车记录,返回订单号给前端。
这个流程必须加@Transactional注解。我最初写的时候漏了这个注解,测试时并发下单导致库存变成负数,后来补上才发现事务的重要性。Spring 的声明式事务默认遇到 RuntimeException 才会回滚,所以 service 里校验失败时要抛出 RuntimeException 或自定义业务异常(继承 RuntimeException),而不是返回一个错误对象就走人。订单创建完成后,我把订单号 order_no 设计成“日期 + 随机数”格式,比如 202506011530120001,这样既避免自增 ID 暴露订单量,也方便人眼识别。
3.4 MyBatis 数据访问与缓存实战
MyBatis 这块我想单独拿出来说,因为面试基本必问。它的工作机制是:SqlSessionFactory 根据 XML 接口生成 mapper 代理对象,调用方法时执行对应 SQL,结果通过 ResultMap 或驼峰映射转成实体类。面试常问的缓存机制,这里也很有说头。一级缓存是 SqlSession 级别的缓存,默认开启。同一个 SqlSession 内执行两次相同的查询,第二次直接走缓存。但 Spring 管理下每次 Mapper 操作通常是独立 SqlSession,所以一级缓存容易被忽略。二级缓存是 Mapper 级别的,跨 SqlSession 共享,需要在 mapper XML 里显式开启:
<cache eviction="LRU" flushInterval="600000" size="512" readOnly="true"/>这里我要提醒一句:二级缓存别随便开。如果你的项目里存在通过多表 join 查询的 SQL,缓存了结果之后,其中一张表的数据更新,另外一张表的缓存不会被自动清空,就会产生脏数据。宠物用品这种商品库存频繁变动的业务,开二级缓存反而容易翻车。我最终只在商品分类表这种极少变更的查询上开了二级缓存,商品和库存一律不走二级缓存。
MyBatis 官方面试题里还有一个高频点:#{}和${}的区别。#{}会生成 PreparedStatement 占位符,能防 SQL 注入;${}是字符串拼接,直接嵌入 SQL,有注入风险。项目里所有查询条件我都用#{},只有在 ORDER BY 的排序字段这种不能走占位符的场景才用${},而且使用时必须做白名单校验,不能直接透传用户输入。
4. 前后端联调与部署上线
4.1 跨域配置与 Axios 请求封装
前后端分离开发中最经典的问题就是跨域。我在 IDEA 里跑后端 8088 端口,Vue 开发服务器默认 8080 端口,前后端端口不同,浏览器就会拦截跨域请求。解决方案有两种,一种是后端加跨域配置,用 Spring Boot 的 WebMvcConfigurer 实现全局跨域:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }另一种是前端用 Vue CLI 的 devServer 代理,在 vue.config.js 里配:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8088', changeOrigin: true } } } }开发阶段我建议用代理方式,这样前端代码里统一请求/api/...,部署后由 Nginx 反代到后端,改动最小。生产环境一定不要开启后端全放行的跨域,会降低安全性。
Axios 请求封装这块,我建了一个 request.js,统一做了四件事:设置 baseURL、请求头带 token、响应拦截器统一处理业务码、401 时跳转登录页。核心代码:
service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) service.interceptors.response.use( res => res.data, err => { if (err.response && err.response.status === 401) { router.push('/login') } return Promise.reject(err) } )统一封装的好处是,业务代码里不用每个接口都写一遍“取 token、塞 header、处理错误”,后期维护成本低很多。
4.2 前端打包与后端启动本地部署
本地部署是最快的验证方式。后端在 IDEA 里直接运行 main 方法即可启动,应用会监听 8088 端口。前端执行npm run build,打包产物在 dist 目录。如果不想用代理,需要 Nginx 做静态资源托管和接口反向代理。一个精简版 Nginx 配置如下:
server { listen 80; server_name localhost; root /home/pet/pet-web/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8088; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这个配置里有几个容易被忽略的细节。try_files $uri $uri/ /index.html;是 Vue Router 历史模式的标配,否则刷新 /cart 页面时会 404——因为 Nginx 在磁盘上找不到 /cart 这个文件。location /api/的 proxy_pass 后面的 URL 不要再带 /api 前缀,否则会变成 /api/api/xxx,这是新手常犯的错误。
4.3 服务器上线部署与数据库初始化
服务器部署我的流程是这样:先装 JDK 8、MySQL 8.0、Nginx。然后 MySQL 里创建 pet_shop 数据库,导入项目附带的 pet_shop.sql 脚本。执行前先确认字符集是 utf8mb4,否则商品描述里的 emoji 会变成问号。后端打 jar 包用mvn clean package -DskipTests,然后通过 systemd 服务方式启动,而不是直接 nohup 裸跑。用 systemd 的好处是:崩溃自动重启、开机自启动、日志统一管理。一个最小 unit 文件如下:
[Unit] Description=pet-server After=network.target [Service] ExecStart=/usr/bin/java -jar /home/pet/pet-server.jar Restart=always User=root [Install] WantedBy=multi-user.target前端构建产物上传到 /home/pet/pet-web/dist,Nginx 按上面的配置指过去。整个过程我建议先本地跑通,再上服务器。不要直接在服务器上联调排错,那样效率太低了。
4.4 常见问题排查速查表
写到这里,我把实操中遇到的典型问题整理成一张速查表,方便直接对照排查:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 后端启动报 Table 'pet_shop.xxx' doesn't exist | 数据库脚本未导入或表名大小写不一致 | 重新执行 SQL,注意 Linux 下表名区分大小写 |
| 前端请求接口 404 | 代理路径或 Nginx 路由配置错误 | 先直接访问后端接口地址验证后端是否正常 |
| 登录成功但访问其它接口 401 | token 未传或过期 | 检查拦截器白名单和 axios 请求头 |
| 中文乱码 | 数据库连接串未指定编码 | URL 加 characterEncoding=utf8,库表字符集用 utf8mb4 |
| 商品列表接口返回慢 | 缺少索引或命中缓存脏数据 | 给 product.category_id 加普通索引,检查缓存配置 |
| Maven 依赖报错 | 镜像源不稳定 | 换阿里云镜像,IDEA 里 Reload All Maven Projects |
另外特别强调一个问题:如果你把前后端都部署在同一台服务器上,一定要注意防火墙只开放必要的端口。80(Nginx)可以对外开放,但 3306 只允许内网访问,不要对公网暴露 3306,否则数据库被爆破的风险很高。后端 8088 端口也要么关掉公网访问,要么只允许 Nginx 所在 IP 访问。
写到最后分享几个真实感受。这套宠物用品交易系统从零到跑通,我最深的体会是:前后端分离项目的问题不是“写不出来”,而是“版本、环境、联调、部署”这四个环节总有一个在给你上课。我第一遍做的时候在 Spring Boot 3.x 的兼容问题上卡了整整两天,后来退回到 2.7.x 才顺畅;联调阶段又被跨域搞得晕头转向,直到熟悉了代理和 Nginx 反代才真正理解“分离”的含义。如果你也想通过这个项目练手,我建议你从数据库设计开始,把表结构理顺了再动手写代码,然后严格按“环境准备 → 模块实现 → 联调部署”的顺序推进。遇到报错不要慌,先看日志,再查文章,最后再改代码——大部分问题的答案都藏在异常堆栈里。希望这份源码和部署教程能帮你少踩几个坑,快速把这个宠物用品交易闭环跑起来。