☰
前后端分离校园店铺实战:SpringBoot+Vue+MyBatis+MySQL全栈开发
2026/10/8 2:27:25 网站建设 项目流程

你问我现在网上各种「前后端分离校园网上店铺」的项目源码都烂大街了,照着抄一遍到底还能学到什么。我的看法是:这类全栈项目最值钱的地方不在代码本身,而在于它把 SpringBoot、Vue、MyBatis、MySQL 这四个东西怎么串成一条完整链路的过程。你以后去公司接触的电商系统,跟这个校园网上店铺其实是一套骨架,无非是业务更复杂、表更多、并发更高。校园店铺这种规模,恰恰适合在可控范围内把数据表、后端接口、前端页面从头到尾跑通一遍。

这篇文章就围绕这个项目展开,我不会贴一整份源码让你复制,而是把设计思路和关键实现拆开讲清楚:为什么技术栈选了这四件套、数据库表怎么设计才对得上业务、后端每个模块的职责边界、前端页面怎么对接接口、最后怎么打包部署。每个环节我都尽量附上实际用过的实现方案和踩坑记录,你可以直接照着这个思路去改造出自己的版本。

先说清楚适合什么人看。你是正在做课程设计、毕业设计的学生,或者刚入门全栈想找一个完整项目练手,这篇内容能省你不少时间。如果你是工作了几年想复习一下前后端分离架构的细节,也可以重点看接口设计、跨域处理、部署配置这几部分。

1. 项目整体设计与技术选型思路

1.1 「四件套」为什么能凑成一套经典组合

校园网上店铺这种系统,技术选型其实没什么悬念。核心就四个字:各司其职。SpringBoot 解决后端开发效率,Vue 解决前端交互体验,MyBatis 解决数据访问灵活性,MySQL 存数据。它们单拎出来都不算新技术,但拼在一起,刚好覆盖一条完整业务链路的全部环节。

SpringBoot 最大的价值是帮后端省掉了配置地狱。早些年做 SSM 框架项目,光是写 web.xml、spring-mvc.xml、mybatis-config.xml、数据源配置、日志配置就能耗掉半天时间,业务代码还没开始写,人先被配置文件搞烦了。SpringBoot 用「约定优于配置」把大部分默认配置直接给你配好,你只要引入对应的 starter,项目就能跑起来。校园店铺这种教学型项目最怕的不是业务复杂,而是环境搭建复杂到劝退。用 SpringBoot,注意力可以集中在 Controller、Service、Mapper 这条主线上。

Vue 的优势在于响应式数据绑定和高频交互场景。网购里最典型的就是购物车:商品数量加减、勾选、总价实时变化,这种操作如果还用传统 jQuery 手动操作 DOM,代码会越写越乱。Vue 的数据驱动视图,意味着数据一变页面自动更新,交互逻辑可以写得很简洁。再一个,Vue 天生组件化,商品卡片、分页组件、轮播图、弹窗提示都能拆成独立组件,管理端和用户端可以复用。

MyBatis 属于半自动 ORM 框架,SQL 由开发者自己写。有人觉得这不如 JPA 省事,但正是因为 SQL 可控,你才能处理多条件动态查询、多表关联、聚合统计这些复杂场景。商品列表要按照价格区间筛选、按销量排序、按分类拼接条件,这种查询用 MyBatis 的 XML 动态 SQL 写起来一目了然。真要在校园店铺项目里用 JPA 硬套,入门成本反而高。

MySQL 就不用多解释了,开源免费,部署简单,性能碰到校园店铺这种数据量完全够用。这套组合下来,前端独立、后端独立、数据库独立,彼此只通过接口交互,正好就是现代前后端分离开发的成熟分工。

1.2 前后端分离到底分离了什么

很多初学者以为「前后端分离」就是前端一个文件夹、后端一个文件夹,这理解太表面了。真正的分离是:两个工程在开发和部署环境上完全独立,唯一的联系是接口文档和约定好的返回 JSON 结构。

这个项目我建议拆成两个工程来管理:

  • 后端工程 campus-shop-server,端口 8080,提供所有业务接口;
  • 前端工程 campus-shop-web,开发时跑在 5173 端口(Vite 默认),通过 axios 请求后端接口。

开发过程中,前端不需要知道后端内部怎么写的,只需知道接口地址和返回字段;后端也不需要关心页面长什么样,只负责返回 JSON 数据。好处很明显:同一套后端接口可以同时支持用户端 H5、管理后台,甚至以后的手机小程序;前后端可以并行开发,效率更高;代码边界清晰,谁出了问题直接定位到哪个工程就行。

但分离也带来一个麻烦:跨域。浏览器有同源策略,前端在 5173 端口访问 8080 端口的接口,直接请求会被拦截报 CORS 错误。解决办法常见有两种:后端加全局跨域过滤器,或者前端开发服务器配置代理。我建议首选前端代理方案,因为生产环境本来就要用 Nginx 做反向代理,开发环境的代理和生产环境的代理思路一致,省得改来改去。具体配置第 4 部分会写。

1.3 角色划分与页面流转设计

校园网上店铺系统至少要支撑三类角色:游客、注册用户、管理员。

  • 游客可以看商品列表、商品详情、搜索商品,但一旦要加购物车、下单、支付,就必须先登录;
  • 注册用户除了购物下单,还能维护收货地址、查看历史订单、取消未发货订单;
  • 管理员从后台登录,负责商品分类管理、商品上下架、订单状态处理、用户禁用启用,以及简单的销售统计。

页面流转可以用一个典型购物流程串起来:商品首页 -> 商品列表/搜索 -> 商品详情 -> 加入购物车 -> 登录验证 -> 确认订单页 -> 提交订单 -> 模拟支付 -> 订单列表页。

管理端的页面则相对独立,从登录进入后台布局,通过侧边栏在商品管理、分类管理、订单管理、用户管理之间切换。前端路由需要用路由守卫把管理端页面保护起来,只允许 admin 角色访问,这一块后面也会讲到。

2. 数据库设计与核心业务模块拆解

2.1 三张核心业务表的关系设计

数据库设计是这个项目的地基。表数量不能太少,太少后面扩展麻烦;也不能一上来就二十多张表,维护成本太高。校园店铺这套系统,9 到 10 张表是比较舒服的规模。我把最核心的三张表拿出来说。

用户表(user)

字段类型说明
idbigint主键
usernamevarchar(50)用户名,唯一
passwordvarchar(100)密码(BCrypt 加密)
nicknamevarchar(50)昵称
avatarvarchar(255)头像路径
phonevarchar(20)手机号
rolevarchar(20)角色标识 user/admin
create_timedatetime注册时间

商品表(goods)

字段类型说明
idbigint主键
category_idbigint所属分类
namevarchar(100)商品名称
covervarchar(255)封面图
detailtext商品详情
pricedecimal(10,2)价格
stockint库存
salesint销量
statustinyint上架状态 1上架 0下架
create_timedatetime发布时间

订单表(orders)

字段类型说明
idbigint主键
order_novarchar(32)订单编号
user_idbigint下单用户
total_pricedecimal(10,2)订单总价
delivery_feedecimal(10,2)配送费
statusint订单状态
receivervarchar(50)收货人
addressvarchar(255)收货地址
pay_typetinyint支付方式
create_timedatetime下单时间

订单和商品之间是多对多关系,一个订单包含多个商品,一个商品也会出现在多个订单里。所以必须有一张订单明细表 order_item,把订单 id 和商品 id 关联起来,同时冗余记录下单那一刻的商品名称、价格、数量、图片。这里很多人不理解为什么要冗余,总觉得查询实时商品表更「规范化」。真实项目里订单明细必须存快照,否则三个月后你查一笔历史订单,商品改了名、下架了、价格变了,历史订单就变成一笔烂账了。电商系统里「快照」这个概念,从这张小表就开始养成。

2.2 校园场景比普通商城多考虑了哪些字段

如果只是套通用商城模板,这个项目和「网上商城管理系统」没有本质区别。既然叫校园店铺,业务设计上就要补几个校园特有细节,这也是答辩或者项目汇报时能体现出思考深度的地方。

第一,收货地址可以扩展成「宿舍区地址」。普通商城收货地址一般就是省市区+街道门牌,校园店铺可以单独建一张 dormitory_address 表,字段包括校区、宿舍楼栋、楼层、门牌号、配送偏好时间。哪怕你前期不建表,至少在订单表里预留一个 address 字段保存完整宿舍信息,展示时才能直观体现「校园」这个词。

第二,支付方式里加一个「一卡通支付」。订单表里的 pay_type 字段,我通常这样约定:1 表示微信支付,2 表示支付宝,3 表示校园一卡通。校园场景下顺手提一嘴「对接一卡通消费系统」,会显得你对业务是有思考的。

第三,计算金额时把配送费单独拆出来。校园店铺经常有「满 30 元免配送费」这类运营规则,所以订单总额 = 商品总额 + 配送费 - 优惠金额。我在订单表里增加 delivery_fee 字段,统计客单价和营收的时候很方便,不用每次反推。

2.3 数据库初始化与测试数据的准备

建库建表的第一步,建议独立创建数据库和专用账号,不要所有项目共用 root 乱搞。实际建库代码我建议这样写:

CREATE DATABASE campus_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE campus_shop; CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), avatar VARCHAR(255), phone VARCHAR(20), role VARCHAR(20) DEFAULT 'user', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有两个硬性注意点。

一是字符集用 utf8mb4,别用 utf8。MySQL 里的 utf8 最多只支持 3 字节编码,遇到生僻字或者 emoji 会直接报错。现在主流客户端和数据保存都默认使用完整 UTF-8,也就是 utf8mb4。

二是引擎用 InnoDB。购物车更新、订单创建、库存扣减都涉及事务操作,InnoDB 支持事务和行级锁,这是 MyISAM 给不了的。虽然测试数据量小感觉不出差别,但从设计习惯上必须从一开始就用对。

测试数据一定要提前备足。我最烦的就是空数据页面调试,列表、搜索、分页、上下架逻辑都需要真实数据支撑。至少准备 2 个管理员账号、5 个普通用户、3 个以上分类、每个分类里 10 个左右商品,这样整个演示流程才顺。

3. 后端 SpringBoot + MyBatis 搭建与核心实现

3.1 项目初始化与依赖管理

后端工程我习惯直接用 Spring Initializr 生成,IDEA 里新建项目时就有这个入口。依赖选择上,下面这几组是必须的:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>

有一个高频坑:MyBatis Starter 版本和 SpringBoot 版本必须匹配。SpringBoot 3.x 要用 mybatis-spring-boot-starter 3.x,SpringBoot 2.x 用 2.x 系列。版本混用最常见的现象就是项目启动时报错找不到 SqlSessionFactory 相关的类,或者 mapper 扫描不到。项目结构我是按这个来组织的:

campus-shop-server/ ├── src/main/java/com/campus/shop/ │ ├── controller/ # 接口层,接收请求、返回结果 │ ├── service/ # 业务逻辑层 │ ├── mapper/ # MyBatis 数据接口 │ ├── entity/ # 实体类 │ ├── common/ # 返回值封装、全局异常、JWT 工具 │ └── config/ # 跨域配置、静态资源映射等 ├── src/main/resources/ │ ├── mapper/ # XML 映射文件 │ └── application.yml

3.2 统一返回、异常处理和 JWT 认证

这部分代码很琐碎,但它是整个后端好不好用的基础。所有 Controller 接口不要直接返回实体对象或者 null,否则前端无法统一判断请求结果。我习惯定义一个 R 类:

@Data public class R<T> { private Integer code; private String message; private T data; public static <T> R<T> ok(T data) { R<T> result = new R<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> R<T> fail(String message) { R<T> result = new R<>(); result.setCode(500); result.setMessage(message); return result; } }

返回格式统一成 {code, message, data},前端 axios 拦截器只需要判断 code 是不是 200,能省掉大量重复代码。所有业务异常也可以统一抛自定义异常,由全局异常处理器 @RestControllerAdvice 捕获后转成统一格式返回,避免后端一崩就把一整页堆栈信息扔给前端。

登录认证我建议直接上 JWT。用户登录成功后,后端返回 token,前端把 token 存 localStorage,之后每个请求在请求头 Authorization 带上 token。后端用拦截器在请求到达 Controller 之前校验 token。这个方案比 Session 轻量,也更贴合前后端分离的架构。

拦截器配置里,白名单一定要排好。用户登录、用户注册、商品列表、商品详情、商品搜索、图片访问这些接口都是游客可访问的,不能拦截:

registry.addInterceptor(new JwtInterceptor()) .addPathPatterns("/**") .excludePathPatterns( "/user/login", "/user/register", "/goods/**", "/category/list", "/file/**" );

还有一个细节:拦截器解析完 token 后,把当前用户 id 放到 ThreadLocal 里,后续 Service 层直接用。注意在拦截器的 afterCompletion 里移除 ThreadLocal,否则线程池复用的时候,下一次请求可能拿到上一个用户的身份,这是一个很难排查的并发小坑。

3.3 核心业务模块的实现要点

商品模块。商品查询只需要做分页、条件搜索、详情返回。分页用 PageHelper,查询前调用 PageHelper.startPage(pageNum, pageSize),后面第一条查询自动拼上 limit 语句。商品列表接口大概长这样:

@GetMapping("/goods/list") public R<PageResult<GoodsVO>> list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword, @RequestParam(required = false) Long categoryId) { PageHelper.startPage(pageNum, pageSize); List<GoodsVO> list = goodsMapper.selectGoodsList(keyword, categoryId); PageInfo<GoodsVO> pageInfo = new PageInfo<>(list); PageResult<GoodsVO> result = new PageResult<>(); result.setTotal(pageInfo.getTotal()); result.setList(pageInfo.getList()); return R.ok(result); }

selectGoodsList 的 XML 里用动态 SQL:

<select id="selectGoodsList" resultType="com.campus.shop.entity.GoodsVO"> select g.*, c.name as category_name from goods g left join category c on g.category_id = c.id <where> g.status = 1 <if test="keyword != null and keyword != ''"> and g.name like concat('%', #{keyword}, '%') </if> <if test="categoryId != null"> and g.category_id = #{categoryId} </if> </where> order by g.sales desc </select>

注意动态 SQL 一定用 MyBatis 的 标签,千万别在 Java 代码里拼字符串。类似 "select * from goods where name like '%"+keyword+"%'" 这种写法既容易 SQL 注入,又难维护。MyBatis 的 #{keyword} 会自动做参数转义,这正是它作为数据访问层的价值。

购物车模块。购物车表虽然简单,但同一个用户对同一个商品加购时,要更新数量而不是插入新记录。这段逻辑在 Service 层写:

Cart cart = cartMapper.selectByUserIdAndGoodsId(userId, goodsId); if (cart != null) { cart.setCount(cart.getCount() + count); cartMapper.updateById(cart); } else { cart.setUserId(userId); cart.setGoodsId(goodsId); cart.setCount(count); cart.setSelected(true); cartMapper.insert(cart); }

订单模块。下单是后端最考验逻辑的地方,要同时做四件事:校验商品状态和库存、计算总价(商品总额加配送费)、创建主订单和订单明细、扣减库存。这四步必须在一个事务里,任何一个失败整体回滚。直接用 @Transactional 注解交给 Spring 管事务。

库存扣减我建议用带条件的更新语句:

update goods set stock = stock - #{count}, sales = sales + #{count} where id = #{id} and stock >= #{count}

这种写法可以防止超卖,因为 update 语句会在数据库层面做原子判断,库存不够就更新 0 行,业务里再检查影响行数即可。校园店铺这种并发量这个粒度完全够用,不必上分布式锁。

管理端统计。管理端首页的数字(用户总数、商品总数、订单总数、销售总额),直接用聚合 SQL:

select count(*) from user; select count(*) from goods where status = 1; select count(*) from orders; select coalesce(sum(total_price), 0) from orders where status in (2, 3);

4. 前端 Vue 页面设计与联调细节

4.1 Vue 项目搭建与路由划分

前端我建议直接用 Vue 3 + Vite + Element Plus。创建命令:

npm create vite@latest campus-shop-web -- --template vue cd campus-shop-web npm install npm install vue-router@4 pinia element-plus axios

你在用 Vue 2 + ElementUI 也没关系,思路完全一样,只是个别 API 名称有差异。目录结构我习惯这样分:

src/ ├── api/ # 所有请求接口封装 │ ├── goods.js │ ├── cart.js │ └── order.js ├── views/ # 页面级组件 │ ├── Home.vue │ ├── Login.vue │ ├── GoodsList.vue │ ├── GoodsDetail.vue │ ├── Cart.vue │ ├── Checkout.vue │ └── admin/ # 管理端页面 ├── router/index.js # 路由配置 ├── store/ # Pinia 状态管理 └── utils/request.js # axios 封装

路由规划上,把游客页面、用户页面、管理页面分开。加了 meta 信息用来做路由守卫判断:

const routes = [ { path: '/', component: Home }, { path: '/goods', component: GoodsList }, { path: '/goods/:id', component: GoodsDetail }, { path: '/login', component: Login }, { path: '/cart', component: Cart, meta: { requiresAuth: true } }, { path: '/orders', component: OrderList, meta: { requiresAuth: true } }, { path: '/admin', component: AdminLayout, meta: { requiresAuth: true, requiresAdmin: true }, children: [ { path: 'goods', component: AdminGoods }, { path: 'orders', component: AdminOrders }, { path: 'users', component: AdminUsers } ] } ];

路由守卫是前端拦截的第一道防线:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.meta.requiresAuth && !token) { next('/login'); } else if (to.meta.requiresAdmin) { const user = JSON.parse(localStorage.getItem('user') || '{}'); if (user.role !== 'admin') { next('/'); } else { next(); } } else { next(); } });

前端守卫只是用户体验层面的保护,真正的安全必须靠后端拦截器再校验一次。两边都做,才是合格的实现。

4.2 axios 封装与登录态保持

如果每个页面都直接调 this.$http.get,代码会非常分散。我习惯先做一个统一的 request.js,把 baseURL、请求头、token 附加、响应拦截都收拢起来:

import axios from 'axios'; import { ElMessage } from 'element-plus'; const request = axios.create({ baseURL: '/api', timeout: 10000 }); request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = token; } return config; }); request.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { ElMessage.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); } return res.data; }, error => { ElMessage.error(error.message || '网络异常'); return Promise.reject(error); } ); export default request;

这里响应拦截器直接返回 res.data,组件里写起来就很清爽了:

const list = await request.get('/goods/list', { params: reqParams });

拿到的就已经是后端 data 字段,业务层不用再写 response.data.data 这种难看的长链。

4.3 Vite 代理解决跨域

开发时前端跑 5173,后端跑 8080,直接请求跨域。最省事的办法是在 vite.config.js 里配代理:

export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } });

配完之后,前端代码里所有请求都写相对路径 /api/xxx,开发时 Vite 代理转发到后端,生产时再用 Nginx 转发,前端代码不需要区分环境。

4.4 商品展示与购物车交互实现

商品列表页用 Element Plus 的 el-card 做卡片展示,再加一个搜索框、分类筛选、分页器。搜索触发重新加载时,把分页参数重置回第一页,这是一个很常见的细节。代码大致是:

const loadData = async () => { loading.value = true; try { const res = await request.get('/goods/list', { params: { pageNum: pageNum.value, pageSize: pageSize.value, keyword: keyword.value, categoryId: categoryId.value } }); list.value = res.list; total.value = res.total; } finally { loading.value = false; } };

购物车数量加减直接修改本地数据,同时调接口更新数据库。这里有个容易踩的坑:后端返回的 price 字段是 BigDecimal,经过 Jackson 序列化后前端有可能拿到的是字符串而不是数字,直接在 JS 里做加减乘除就会变成字符串拼接,总价很容易算错。解决方案是后端给价格字段加 @JsonFormat 相关配置统一转成数字,或者前端先 parseFloat 再计算。我建议前者,因为金额计算这种事,前端本来就不该做太多浮点运算。

5. 部署与常见问题排查

5.1 本地开发环境准备清单

整套环境,我列一个参考版本:

组件版本建议说明
JDK8 或 17SpringBoot 2 用 8,SpringBoot 3 用 17
MySQL5.7 或 8.08.0 兼容性更好
Node.js16 及以上配合 Vite 使用
Maven3.6 以上后端依赖管理
IDEIntelliJ IDEA前后端都可以写

启动 MySQL 后导入数据库脚本。Windows 安装 MySQL 最常见的坑是初始化后密码设置不明确、或者服务名冲突。建议用环境变量把 MySQL 的 bin 目录加进 PATH,方便直接在命令行敲 mysql 命令。导入脚本:

source /path/to/campus_shop.sql;

然后修改后端 application.yml:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver

MySQL 8 的驱动类名是 com.mysql.cj.jdbc.Driver,不是早期的 com.mysql.jdbc.Driver。URL 里的 serverTimezone 必须配,否则启动时直接报时区错误。

接着启动后端,再启动前端。前端启动前先 npm install。npm 慢的话可以切换国内镜像源或用 pnpm,差别不大。验证方式很简单:浏览器访问 http://localhost:8080/api/goods/list,能看到 JSON 说明后端通;访问 http://localhost:5173 能打开首页说明前端通;登录后打开购物车能加载数据,说明前后端连通。

5.2 上线的标准打包流程

本地能跑只是第一步,交付给别人或者部署到服务器,要分别打包前后端。

后端打包:

mvn clean package -DskipTests

构建后在 target 目录生成 campus-shop-server-1.0.0.jar。部署时用:

java -jar campus-shop-server-1.0.0.jar

想让它在服务器后台长期运行,用 nohup:

nohup java -jar campus-shop-server-1.0.0.jar > app.log 2>&1 &

前端打包:

npm run build

生成到 dist 目录。把 dist 里的静态文件交给 Nginx。Nginx 配置贴一个最简可用版本:

server { listen 80; server_name localhost; location / { root /usr/share/nginx/html/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } }

location / 里的 try_files 配置非常关键。Vue 用的是 history 路由,用户直接访问 /order/123 这类深层页面时,如果 Nginx 按路径找文件必然 404,必须 try_files 重写到 index.html,由前端路由接管。

部署全链路是这样的:用户请求 80 端口 -> Nginx 返回前端静态文件 -> 页面里的 /api 请求被 Nginx 转发到 8080 端口 -> 后端返回 JSON -> 前端渲染。整个过程前后端各管各的,互不干扰。

5.3 常见问题速查表

现象原因解决办法
启动报时区错误MySQL 连接参数缺 serverTimezoneURL 加 &serverTimezone=Asia/Shanghai
前后端端口跨域前端域名/端口和后端不同开发用 Vite 代理,部署用 Nginx 转发
前端展示不了上传的图片图片路径没有映射为静态资源后端加 WebMvcConfigurer 把上传目录映射为 /file/**
登录接口一直报「登录失败」密码明文/加解密方式不一致后端统一用 BCrypt 加密和校验
页面显示中文乱码客户端编码和数据库编码不一致统一 utf8mb4,连接参数加 characterEncoding=utf8
打包后的前端刷新深层路由 404没配置 try_filesNginx 加 try_files $uri $uri/ /index.html
商品列表搜不到数据接口参数名和前端不一致统一 pageNum、pageSize、keyword 等字段名
jar 包部署内存不足服务器内存小java -jar 加 -Xms64m -Xmx256m

前三个坑最常出现。尤其是图片上传后的静态资源映射,很多人把图片存到本地某个文件夹就不管了,前端当然访问不到。我当时的配置是这样的:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath = System.getProperty("user.dir") + "/upload/"; registry.addResourceHandler("/file/**") .addResourceLocations("file:" + uploadPath); } }

再补一个不建议抓瞎的问题:Vue 3 用 Pinia 保存用户信息时,页面刷新后 store 数据会清空。要保证登录状态不丢,把 user 对象也存一份 localStorage,应用初始化时再恢复 store。这一步不做,你会看到很诡异的现象:登录后一切正常,刷新一下,购物车进不去了,又得重新登录。

根据我个人这两年的经验,校园网上店铺这个项目最难的部分真不是什么高级框架特性,而是把数据库、后端、前端这条链路完整打通。很多同学报错就上网搜,搜到一个解决办法套上去,没有去理解为什么。你只要把上面这几个最关键的点逐步跑通,比如统一返回格式、JWT 拦截器、事务扣库存、Axios 封装、Nginx 代理,整个项目就算真正内化成你自己的能力了。做完之后你还可以继续往深了加东西,比如文件用 OSS 存储、订单加定时任务自动关闭、管理端加图表统计,这些都是建立在骨架稳定之上的扩展,但那就是另一个话题了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询