news 2026/9/26 20:09:38

SpringBoot+Vue3前后端分离实战:明星周边商城项目全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue3前后端分离实战:明星周边商城项目全解析

1. 项目定位与整体设计思路

做明星周边产品销售网站这个需求,在课程设计、毕业设计和接私活里其实非常常见。核心用户是粉丝群体,他们要的不是什么高大上的供应链系统,而是“能在手机上看到喜欢的艺人周边、能加购、能下单、能查物流、偶尔能发表一下评价”的完整闭环。“星之语”这个项目我组装过一版差不多的,下面把拆解思路和落地过程完整写出来,适合有Java基础但还没独立做过完整前后端分离项目的朋友参考,也适合拿来当作面试的项目经历讲。

“星之语”本身是一个典型的B2C电商系统,相比普通商城模板,它的差异点在于“明星周边”这个垂直场景:商品以小卡、海报、应援服、手办、专辑配置等为主,商品规格多变(版本、尺寸、特典卡与否),用户对商品图、详情描述、周边套装组合比较敏感,订单量小但频率高。所以系统设计上不需要搞复杂的库存博弈和价格策略,重心放在:清晰的商品展示、灵活的规格选择、顺畅的下单流程、以及后台对商品和订单的管理能力。

前后端分离是这套架构里我坚持的选择。SpringBoot提供纯后端API,Vue3负责页面渲染和交互,两者通过JSON通信。好处很直接:前端开发和后端开发可以并行;后续如果要出小程序端或者App端,后端API可以原样复用;部署上也灵活,前端丢Nginx,后端打jar包,互不拖累。坏处也有,就是需要处理跨域、鉴权、接口联调这些事情,文章里会重点讲。

技术栈选型上,SpringBoot 2.x + MyBatis + MySQL 是国内Java后端最保守也最稳妥的组合,没有之一。SpringBoot的自动配置省掉大量XML配置,MyBatis把SQL交还给程序员,复杂的多表查询和报表统计反而更好控制,MySQL则足够支撑这个量级的业务。前端选Vue3而不是Vue2,是因为组合式API在组件复用和逻辑组织上确实更清晰,而且新项目没理由守着旧版本不放。前后端加起来技术栈足够有含金量,又不像微服务那套让人望而却步。

2. 数据库设计与核心功能模块

2.1 表结构设计:从“买周边”这个动作倒推

数据库设计我习惯先画业务流程图,把“用户进站 -> 浏览商品 -> 加入购物车 -> 提交订单 -> 支付 -> 收货 -> 评价”这条主链路跑一遍,然后反推需要哪些表。核心表如下:

  • user:用户表,字段包括用户名、密码(BCrypt加密存储)、昵称、头像、手机号、邮箱、注册时间、状态。
  • product:商品表,包括商品名称、副标题、封面图、轮播图、详情富文本、所属艺人或IP、分类、价格、库存、销量、上下架状态、创建时间。
  • product_spec:商品规格表,因为明星周边经常有“普通版”、“豪华版”、“特典版”之分,价格和库存都不同,所以必须把规格独立出来,而不是直接在商品表里写死价格。
  • cart:购物车表,关联用户和商品规格,记录数量、勾选状态。
  • orders:订单表,包含订单编号、用户ID、总金额、实付金额、收货人、收货电话、收货地址、订单状态、支付方式、支付时间、创建时间。
  • order_item:订单明细表,一个订单对应多个商品,记录商品快照(名称、图片、规格、单价、数量、小计),为什么要快照?因为商品信息后续可能修改,但订单历史必须保持下单时原样。
  • delivery_address:收货地址表,一个用户可以维护多个地址,下单时选择一个。
  • review:评价表,关联订单明细和商品,内容包括评分、内容、图片列表、回复。
  • admin_user:后台管理员表,和前台用户分开,避免权限混淆。

这里有个细节很多人会忽略:订单金额不要直接存“总价”,而是保留total_amount(商品原价合计)、discount_amount(优惠金额)、freight_amount(运费)、pay_amount(实付金额)四个字段。后面一旦要做满减活动或者运费策略,只改计算逻辑,不动表结构。

订单状态我建议用数字枚举而不是字符串,代码里统一管理:0待付款,1待发货(已付款),2待收货(已发货),3已完成,4已取消,5退款中。用数字的好处是存储空间小、索引效率高、前端只需要映射字典即可。

2.2 前台核心链路:浏览、加购、下单

前台页面按用户操作路径来拆,基本是这几个页面:首页、商品列表页、商品详情页、购物车页、确认订单页、订单列表页、订单详情页、个人中心。

首页不必做得太重,放一个搜索框、分类导航、轮播图、推荐商品列表就够了。商品列表页重点是筛选和分页,按艺人/IP、分类、价格区间、销量、上架时间排序。我用MyBatis的分页插件配合前端的分页组件来做,后端返回pageNum、pageSize、total、list四个字段,前端拿到直接渲染分页条,接口设计很固定。

商品详情页是核心。除了基本信息,还要处理规格选择逻辑:用户选“版本-豪华版”时,价格和库存从product_spec表动态取;库存不足要置灰;加购按钮请求后端,后端校验库存后写入购物车表。这里容易出现并发问题,实际生产环境要用乐观锁(UPDATE product_spec SET stock = stock - #{count} WHERE id = #{id} AND stock >= #{count}),课程设计阶段可以用同步代码块模拟,但SQL写出来更有亮点。

确认订单页可以顺便做地址管理和留言。提交订单是事务操作:生成订单主表记录 -> 生成订单明细 -> 扣减库存 -> 清空购物车对应商品。任何一个环节失败,全部回滚。为了演示方便,支付我用的是模拟支付,点击“立即支付”直接走支付回调接口把订单状态改成待发货,同时记录支付时间。真正接微信或支付宝支付,原理也是回调改状态,只不过多了签名校验和金额核对。

2.3 后台管理模块:别把它做成纯CRUD

后台管理是面试时最容易展开讲的部分。很多课程设计项目后台就是对着每张表做一个增删改查页面,这没有错,但显得单薄。我在“星之语”里给后台做了三个有区分度的功能:

第一是商品上下架与库存预警。商品列表里直接显示“在售/下架”切换按钮,库存低于某个阈值时列表行变红,这个逻辑在后端查询时算好一个stockStatus字段返回。第二是订单处理流程。管理员看到待发货订单,点击发货时填写物流公司和物流单号,订单状态自动推进到待收货;用户点击确认收货或超时自动确认后进入已完成。第三是简单的数据看板,用几条SQL查询统计今日订单数、今日销售额、热销商品TOP5,前端用Vue3加ECharts渲染柱状图和折线图。这些功能不难,但直接让项目从“管理系统”变成“带业务闭环的系统”。

权限方面,后台接口用SpringBoot拦截器校验admin_token,没有带token或者token非法的请求统一返回401。前端用Vue Router的全局前置守卫,判断本地有没有token,没有就跳到登录页。这种前端路由守卫配合后端接口鉴权的方式,是前后端分离项目里最通用的权限控制方案,比单纯依赖后端返回页面靠谱得多。

3. 关键实现细节与实操过程

3.1 SpringBoot集成MyBatis的配置细节

项目初始化直接用Spring Initializr生成,依赖选Spring Web、MyBatis Framework、MySQL Driver、Lombok就够了。application.yml里三个核心配置写清楚:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/star_speak?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.starspeak.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

这里有几个坑必须说明。第一是serverTimezone=Asia/Shanghai,不写会报时区错误,尤其MySQL 8.x版本。第二是map-underscore-to-camel-case必须开,否则数据库的create_time映射不到实体的createTime,查出来全是null,排查半天都不知道问题在哪。第三是log-impl设置为StdOutImpl,控制台能直接打印SQL和参数,开发阶段排查问题利器,上线前记得去掉或者改成Slf4j。

Mapper接口和XML文件放同一个包路径或者用mapper-locations显式指定都可以。我个人推荐单独resources/mapper目录放XML,因为SQL复杂时XML的语法高亮和格式化比注解舒服得多,而且热部署对XML的支持也更稳定。写Mapper接口时,参数超过一个建议用@Param显式命名:

List<OrderItem> selectByOrderIdAndStatus(@Param("orderId") Long orderId, @Param("status") Integer status);

不加@Param的话,MyBatis只能用#{param1}、#{param2}这种可读性极差的引用方式,一旦SQL复杂了自己都看不懂在查什么。

3.2 MyBatis分页插件PageHelper的用法与原理

分页插件在热搜词里出现频率极高,面试也经常被问到底层实现。PageHelper是MyBatis最常用的分页插件,用法极其简单,复杂的是理解它背后的机制以及注意它的坑。

引入依赖:

<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency>

如果是SpringBoot 2.x,这个starter会自动配置好拦截器,不用手动加@Bean。使用方式是在查询之前调用PageHelper.startPage(pageNum, pageSize),然后执行紧跟其后的第一条查询语句:

public PageInfo<ProductVO> queryProductPage(ProductQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); List<ProductVO> list = productMapper.selectProductList(query); return new PageInfo<>(list); }

返回的PageInfo里有total、pageNum、pageSize、list、pages这些字段,前端要的全都有。它底层的原理是拦截器模式:startPage把分页参数放到ThreadLocal里,MyBatis执行Executor的时候被PageInterceptor拦截,改写原始SQL加上LIMIT,同时自动发起一条COUNT查询获取总量。也就是说,你写SQL时完全不用手写LIMIT,插件帮你搞定。

但这里有几个高频坑。第一,startPage之后必须紧跟Mapper查询,中间不能有任何其他数据库操作,否则分页参数可能套到错误的SQL上,出现“查询A却把B分页了”的诡异结果。第二,PageHelper只对紧跟它的第一条查询生效,如果你在Service里先查了别的表再查目标表,分页会失效。第三,分页查询的结果如果做了二次封装,比如把List<ProductVO>换成别的结构,PageInfo里的分页信息可能丢失,建议直接复用PageInfo里的list,不要重新包装。

最后补充一点,很多人问分页插件和直接写LIMIT哪个好。我的观点是:单一列表页手写LIMIT完全没问题,但像后台订单列表这种还要按状态筛选、按时间排序的复杂查询,PageHelper能省掉大量重复的COUNT和LIMIT代码,提高开发效率。搞清楚原理之后,用起来才有底气。

3.3 Vue3组合式API与前端工程化搭建

前端我用的Vite + Vue3 + Pinia + Vue Router + Axios + Element Plus这个组合。Vite相比Webpack最大的感受就是启动速度确实是秒开,开发体验完全不同。用Vite创建项目一行命令:

npm create vite@latest star-speak-web -- --template vue

创建完进入目录安装依赖,然后逐个安装必要插件。这里强烈建议用npm而不是cnpm,cnpm虽然快,但经常出现莫名其妙的依赖版本错乱,安装完跑不起来然后陷入“删node_modules重新装”的循环。我这几年前端装依赖踩的坑,十有八九都是包管理器混用造成的。

Vue3开发里最重要的是组合式API的习惯转变。以前Vue2是data、methods、computed分开写,现在全部放进<script setup>里,逻辑按功能组织。比如商品列表页,过滤条件、列表数据、加载状态、分页信息可以写在一起:

<script setup> import { ref, reactive, onMounted } from 'vue' import { getProductPage } from '@/api/product' import { ElMessage } from 'element-plus' const loading = ref(false) const query = reactive({ pageNum: 1, pageSize: 12, categoryId: null, keyword: '', orderBy: 'default' }) const total = ref(0) const productList = ref([]) async function fetchProductList() { loading.value = true try { const res = await getProductPage(query) productList.value = res.data.list total.value = res.data.total } finally { loading.value = false } } function handleSearch() { query.pageNum = 1 fetchProductList() } function handlePageChange(page) { query.pageNum = page fetchProductList() } onMounted(fetchProductList) </script>

组件通信在Vue3里也比较有讲究。“星之语”项目里:父组件给子组件传数据用defineProps,子组件往父组件发消息用defineEmits,跨页面共享用户信息用Pinia的storeToRefs,复杂表单里的局部状态用ref包一个对象。我建议不要在项目里到处用provide/inject,全局状态越少越好,不然代码可维护性会迅速下降。

Axios封装这块我可以直接给出模板。一定要创建独立的request.js文件,配置baseURL、请求拦截器、响应拦截器。请求拦截器里从localStorage拿token放到Authorization头;响应拦截器统一处理后端返回的code,code !== 200就ElMessage报错并reject,HTTP 401就清token跳登录页。这样业务代码里完全不用处理鉴权逻辑,只管写页面。

3.4 前后端联调:跨域、鉴权和接口约定

前后端分离项目联调时第一个拦路虎就是跨域。开发环境下前端跑在http://localhost:5173,后端在http://localhost:8080,浏览器同源策略直接拦截。解决方式有前端代理和后端CORS两种,我建议开发阶段两条都用上。

后端CORS最简单的方式是加一个@Configuration类:

@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); } }

注意allowedOriginPatterns("*")和allowCredentials(true)要搭配使用,如果写allowedOrigins("*")在SpringBoot高版本里会报错,因为不允许带凭证的跨域请求使用通配符来源。

前端Vite代理更简单,在vite.config.js里配置:

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

然后axios的baseURL就写/api,请求到/api/xxx会被Vite转发到后端http://localhost:8080/api/xxx。这样浏览器看到的请求是同源的,跨域彻底不存在,后端也可以不用配CORS。生产环境部署时,Nginx把/api反向代理到后端jar包,前端静态文件直接交给Nginx托管,整个链路就通了。

鉴权这部分用的JWT。用户登录成功后,后端生成token返回给前端,前端存到localStorage。请求拦截器加上token,后端用拦截器统一校验。我写了一个AuthInterceptor,实现HandlerInterceptor接口,在preHandle里放行登录和注册接口,其余接口解析token,解析失败返回401。需要注意的是,SpringBoot的拦截器默认不拦截静态资源和错误页面,但会拦截所有/api/**下的请求,所以接口路径设计一定要规范,比如登录接口/api/auth/login要在白名单里放行。

接口约定方面,我统一用Result<T>封装:code、message、data三个字段。成功时code=200,失败时code=500,未登录时code=401。一开始就把规范定好,前后端并行开发的时候不用频繁对接口文档。

4. 常见问题与排查技巧实录

4.1 分页查询total恒为0或数据重复

PageHelper刚上手时,很多人会遇到两种典型问题。一种是total始终为0,另一种是数据重复或偶尔查出来的数据不对。先说第一种,多半是PageInfo拿错了位置。如果你在startPage之后先调用了别的Mapper查询,再执行目标查询,分页插件会把分页参数用在了错误的查询上,目标查询就没分页,返回的list就是全量数据,PageInfo的total自然不对。解决办法很简单:startPage下面立刻接目标查询,中间不掺任何其他数据库操作。

第二种数据重复,常见于关联查询。如果商品列表SQL联了product_spec表,一个商品对应多条规格,那查询结果本身就会产生重复行。分页插件只是对SQL加LIMIT,它不会帮你纠正JOIN带来的结果膨胀。正确做法是分页查询先只查product主表数据,拿到当前页的商品ID集合后,再用WHERE id IN (...)查规格和图片信息,在内存中组装。课程设计里数据量不大,这种内存组装完全可行,而且SQL更清晰。

4.2 MyBatis缓存引发的“灵异事件”

MyBatis的二级缓存是面试高频考点,也是实际开发里容易出幺蛾子的地方。一级缓存是SqlSession级别的,同一个事务里两次查询相同SQL会复用结果;二级缓存是Mapper级别的,跨SqlSession共享。默认情况下,MyBatis的一级缓存是开启的,二级缓存如果没有在XML里显式配置<cache/>,默认不开启。

在一个事务里,先查商品信息,改了库存,再查商品信息,如果缓存生效,查出来的可能是旧数据。我在“星之语”项目里吃过一次亏:后台修改商品库存后,前台列表页数据不更新,过一会才好。排查后发现是二级缓存把商品信息缓存住了,修改操作没有触发缓存刷新。

解决办法一般来说是:涉及频繁变更的数据不要开二级缓存,或者把flushCache设为true。考虑到这个项目的业务场景,我在商品查询Mapper上直接限制了缓存行为。如果你在面试里被问到“MyBatis一级缓存和二级缓存的区别”,可以参考这样的回答路径:一级缓存默认开启,作用域是SqlSession;二级缓存需要手动开启,作用域是Mapper Namespace;缓存机制不是银弹,误用会读到脏数据,实际项目中要根据业务决定是否开启。

4.3 跨域配置失效与拦截器放行问题

后端配了CORS,前端请求还是报跨域,这是排查次数最多的一个场景。几个隐藏点:第一,CORS预检请求是OPTIONS方法,如果后端拦截器把OPTIONS请求拦截住了,前端连预检都过不去,自然报跨域。所以拦截器里必须放行OPTIONS请求。第二,SpringBoot过滤器(Filter)的执行顺序在拦截器(Interceptor)之前,如果有全局过滤器对请求做了处理,也可能干扰CORS头。第三,用allowedOriginPatterns("*")的时候,注意浏览器对Access-Control-Allow-Origin不能同时返回多个值,如果你在Nginx层也配置了CORS头,后端又配了一次,响应里可能出现重复头,浏览器直接不认。

排查顺序建议是:先在浏览器控制台看CORS报错的完整信息,再用curl手动带Origin请求头验证后端是否返回了正确的CORS头,最后检查拦截器和过滤器的放行逻辑。按这个顺序走,基本十分钟内能定位问题。

4.4 前端冷启动阶段的典型问题

Vue3项目启动不起来,常见原因集中在Node版本和依赖版本冲突。Vite 5要求Node 18以上,npm install时如果本机Node版本过低,会直接报Dynamic require of "vue" is not supported这类错误。解决方案是升级Node或者用nvm切换版本。另外Element Plus安装后别忘了引入样式,两种方式:全量引入import ElementPlus from 'element-plus'然后app.use(ElementPlus);或者按需引入用unplugin-vue-components插件。全量引入在课程设计阶段完全够用,开发效率更高,不必为了性能去折腾按需加载。

路由守卫和Pinia的配合也容易出错。如果用户在刷新页面后直接访问/product/1这种详情页,由于登录状态是存在Pinia里的,刷新后Pinia被重置,路由守卫拿不到用户信息,可能把已登录用户误判为未登录。解决办法是路由守卫里除了查Pinia,还要检查localStorage里的token,token存在就放行,用户信息等进入页面后再异步拉取。前端里“刷新丢状态”是最常见的遗漏点,能主动处理这个细节在面试里是加分项。

5. 部署运行与面试考察点

5.1 本地运行与打包部署的完整步骤

如果你拿到的是别人写的项目,第一件事不是启动,而是先看README或者数据库脚本。我整理出一套通用的运行步骤:

  1. 创建数据库star_speak,导入sql/star_speak.sql脚本,确认表名和字段名跟代码里一致。
  2. 修改后端application.yml里的数据库账号密码。
  3. 启动后端:mvn spring-boot:run或在IDE里直接运行主类。看到“Tomcat started on port 8080”说明后端就绪。
  4. 前端进入star-speak-web目录,执行npm install,然后npm run dev。Vite会打印出访问地址。
  5. 打开前端地址,正常应该能看到页面。如果页面白屏,优先看浏览器控制台有没有请求报错,多半是接口地址或跨域问题。

生产部署时,后端打包成jar:

mvn clean package -DskipTests java -jar star-speak-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod

前端打包成静态文件:

npm run build

打包产物在dist目录,把dist里的文件扔到Nginx的html目录,配置一个反向代理:

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

try_files $uri $uri/ /index.html这句必须加,否则前端路由用Vue Router的history模式时,刷新页面会404。

5.2 面试和课程设计中的高频考察点

这套项目如果用来写简历或面试,几个关键技术点是考官大概率会深挖的。SpringBoot层面,自动配置原理、@SpringBootApplication的内部结构、Starter机制、内嵌Tomcat原理。MyBatis层面,#{}和${}的区别(前者预编译防止SQL注入,后者直接拼接有注入风险)、分页插件原理、一级和二级缓存。Vue3层面,ref和reactive的区别、组合式API相比选项式API的优点、Vue Router守卫、Pinia和Vuex的差异。MySQL层面,索引失效场景、LEFT JOIN和INNER JOIN的区别、事务隔离级别。

这里我建议你把“项目难点”好好准备一下。不要只说“我做了购物车”,要说“购物车在用户未登录状态可以本地暂存,登录后合并到服务端,后端通过事务保证订单创建和库存扣减的一致性”。这种表达方式让面试官能直接感受到你确实理解自己在做什么,而不是背概念。

另外,讲项目的时候主动带出“如果数据量大了怎么办”这个问题。比如分页查询优化可以从LIMIT offset改成基于游标的分页,商品列表可以加Redis缓存,订单表可以按时间分表。这些扩展方案并不是让你现在去实现,而是展示你有全局思维。

最后说一点实用的体会。做这类前后端分离项目,最关键的不是某一项技术用得多深,而是整个链路是完整跑通的:注册登录、商品浏览、加购下单、后台发货、状态流转,每一步都有数据流转和页面反馈。当你把一个“能演示、能讲清、能上线”的闭环系统跑通一遍之后,SpringBoot和Vue3的许多零散知识点会自动串起来。后面如果你想在这个项目上继续加东西,优先做两件事:一是接入真实的支付回调,二是用Redis改造商品热门列表。这两块做完,项目的完整度和面试说服力还会再上一个台阶。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 19:58:45

从 WorkBuddy 到 TaoToken:国产 Agent 的工作流入口,藏在 settings.json 里

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 19:52:15

AIOps 落地第四期:基于时序强化学习的多集群动态流量调度

AIOps 落地第四期&#xff1a;基于时序强化学习的多集群动态流量调度在企业级跨地域多活&#xff08;Multi-Region Multi-Cluster&#xff09;架构演进到成熟阶段后&#xff0c;技术团队往往在全国甚至全球部署了多个生产集群&#xff08;如 k8s-prod-east 华东机房、k8s-prod-…

作者头像 李华