☰
Spring Boot+Vue旗袍商城系统开发实战:从数据库设计到部署优化
2026/10/10 3:14:52 网站建设 项目流程

1. 项目概述:Spring Boot + Vue 做旗袍商城,到底在做什么

拿到“Java基于Spring Boot+Vue的旗袍商城系统”这个标题时,我第一反应是:这看起来是个典型的电商项目,但真正动手后才发现,旗袍这种垂直品类和卖数码、卖零食的通用商城差别很大。旗袍背后是定制、面料、手工工艺、尺码体系这些偏传统行业的逻辑,如果照搬一套普通商品系统,做着做着就会发现很多字段和流程根本对不上。

这个系统本质上是一个前后端分离的垂直电商平台:后端用Spring Boot提供RESTful接口,前端用Vue构建单页应用,支撑商品展示、用户登录、购物车、订单结算、后台管理这些核心链路。它能解决的问题很直接——帮助中小旗袍品牌、定制工坊或者线下门店把商品和服务搬到线上,让用户可以在线选款、按需定制、下单并跟踪订单状态。

适合参考这个项目的人主要有三类:正在做毕业设计或个人作品集的Java学习者;需要为传统服饰商家搭建商城的前端/全栈开发者;以及想了解垂直电商如何做业务建模的产品或开发人员。对于第一类人,这套系统的技术栈足够主流,又能充分展示从接口设计到前后端联调的完整能力;对于后两类人,重点关注它如何处理定制类商品的字段扩展和订单状态流转,这比代码本身更有参考价值。

我在下面会把整个系统的拆解思路、数据库设计、后端核心实现、前端联调、部署优化和踩坑记录全部过一遍,内容偏工程实践,你看完可以直接拿去开一个新项目。

2. 业务建模:旗袍商城的模块划分与数据库设计

2.1 为什么选择前后端分离,而不是传统单体模板

很多人会问,一个商城为什么非要拆成Spring Boot + Vue两个工程?直接用Thymeleaf或者JSP写模板,一个人也能维护,开发速度不是更快吗?

放到旗袍商城这个场景里,前后端分离的优势会变得很明显。第一,商品详情页、购物车、订单中心有大量异步交互,用Vue处理交互状态比刷新页面流畅得多;第二,未来如果要做小程序或者管理后台的App端,后端接口可以直接复用,不需要重写一套服务端代码;第三,传统模板在多人协作时经常出现前端改页面、后端也要跟着改模板的情况,而前后端分离后,两边只需要约定接口结构,开发节奏互不干扰。

当然,拆分成两个工程会带来跨域、token鉴权、部署复杂度上升这些问题。但这些都是有成熟方案的,后面第四章和第五章会细讲。对于一个需要长期迭代的垂直电商项目,前后端分离的收益远大于成本。

2.2 功能模块清单:用户端、商家端、平台端要分清楚

做电商系统最容易犯的错误是所有角色共用一套页面和逻辑。旗袍商城的角色至少有用户、商家、平台管理员三类,三者的操作边界差异很大。

整个系统可以划分成这几个模块:

  • 用户端:首页、商品分类列表、商品详情、购物车、订单结算、个人中心、收货地址管理、登录注册。
  • 商家端:商品管理(发布、上下架、库存编辑)、订单处理(发货、填写物流单号)、售后处理。
  • 平台管理端:用户管理、商家审核、订单监管、数据统计、分类管理、系统参数配置。

从技术实现角度看,用户端是Vue单页应用,后台管理可以单独做一个子应用或者用路由做权限区分。我建议后台管理不要和用户端混在一个页面里,因为两边的页面风格、交互模式完全不同。权限控制上,后端接口不仅要区分登录状态,还要区分当前用户是否拥有某个角色的访问权限,这一步在接口层面用拦截器做校验即可。

2.3 数据库设计:通用电商表结构加上旗袍定制扩展

先看基础表结构。一个商城最少要有用户表、商品表、商品分类表、购物车表、订单表、订单商品表、收货地址表。下面是核心字段设计:

表名关键字段说明
userid, username, password, phone, role_type, avatar, statusrole_type区分用户、商家、管理员
categoryid, parent_id, name, sort_order, deleted支持多级分类,比如“旗袍上衣”、“改良旗袍
productid, category_id, name, subtitle, main_image, images, price, stock, status, deleted商品主表,基础信息
product_attrid, product_id, attr_name, attr_value, extra_price存放旗袍定制属性,如领型、袖长
cart_itemid, user_id, product_id, product_attr_json, quantity, checked购物车项
ordersid, order_no, user_id, total_amount, status, receiver_info, create_time订单主表
order_itemid, order_id, product_id, product_name, product_image, product_attr_json, price, quantity订单快照
addressid, user_id, receiver_name, receiver_phone, province, city, detail, is_default收货地址

这里我特别想强调product_attr_json这个字段。旗袍不像普通T恤只有一个固定库存,同一款式可能有不同的领型、袖长、盘扣样式、面料选择,甚至刻字绣花。不同组合的额外加工费也不一样。如果每个属性组合都生成一个SKU记录,后台维护成本会非常高,因为很多组合实际根本不会有人选。

我的做法是:把用户选择的属性JSON直接存到购物车项和订单项里,比如{"collar":"低领","sleeve":"七分袖","embroidery":"绣名字","extraPrice":80}。商品主表只保存基础起售价,前端在选择属性时计算最终价格。这样既能展示定制流程,又不至于把SKU表炸掉。等到订单量大了需要做库存精细化管理,再单独引入SPU/SKU模型也来得及。

3. Spring Boot后端实战:从登录到订单的全链路实现

3.1 初始化工程与核心依赖

后端工程我习惯用Spring Boot 2.7.x搭配MyBatis-Plus,不选3.x是因为公司内部很多镜像还没跟上,你自己新起项目用3.x也没有问题。MyBatis-Plus帮我省去了大量单表的CRUD代码,配合条件构造器写查询非常顺手。

pom.xml里的核心依赖大致是这个样子:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> </dependency>

application.yml有几个关键点需要提前踩平:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/qipao_mall?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

注意MyBatis-Plus的逻辑删除配置。项目里我统一给核心业务表加了deleted字段,这样删除商品和分类都走逻辑删除,前端不会出现“商品突然消失、历史订单无法展示商品名”的情况。启用逻辑删除后,所有MyBatis-Plus自动生成的查询都会带上deleted=0条件,非常省心。

3.2 用户登录与JWT权限拦截

一个商城系统登录方案的底线是:不能让每个接口都依赖Session,前后端分离环境下推荐用JWT令牌。

简单做个总结:用户提交手机号密码之后,后端校验通过,生成一个带userId和角色信息的token返回给前端。前端把token存到localStorage,并在请求头里带上Authorization: Bearer token,后端写一个拦截器统一解析。

值得注意的是,JWT里不要放太多敏感信息,也不要设过长过期时间。旗袍商城后台管理端建议token过期时间设置为2小时,用户端可以设置为7天,这样体验较好。如果后续要支持“记住我”,再单独调长过期时间,而不是一开始就无脑设成一个月的token。

核心拦截器逻辑可以这样写:

@Component public class JwtInterceptor implements HandlerInterceptor { @Autowired private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); } Long userId = jwtUtil.parseToken(token); if (userId == null) { response.setStatus(401); response.setContentType("application/json;charset=utf-8"); response.getWriter().write("{\"code\":401,\"msg\":\"登录已过期,请重新登录\"}"); return false; } request.setAttribute("userId", userId); return true; } }

然后是角色权限拦截。对于商家端接口,比如发布商品、处理订单,不能只验证登录状态,还要验证角色。我通常再写一个RoleInterceptor或者在拦截器里读取用户角色信息,用注解@RequireRole("merchant")标记接口。这样admin接口和商家接口不会越权访问。

3.3 商品、购物车、订单三个核心模块的写法

商品查询是商城流量最高的接口,建议直接使用MyBatis-Plus分页查询。前端传递pageNum、pageSize、categoryId、keyword、sortType,后端构造LambdaQueryWrapper查询。分类筛选时要注意,如果选了父分类,需要把子分类的ID也查出来,否则商品会漏掉。

添加购物车接口的业务逻辑比较简单,但有一个必须处理的点:同一个用户、同一个商品、同一套定制属性组合不能重复插入记录,应该做合并数量。判断“同一条属性组合”用最朴素的方式就行——比较product_attr_json排序后的字符串是否相等。

订单创建是整个系统里最容易出问题的地方。从下单流程看,需要完成这几步:校验购物车项是否属于当前用户、校验商品是否还有库存、计算订单金额(包含定制属性额外价格)、更新库存、生成订单和订单项、清空已结算的购物车项。

这里必须保证事务一致。我推荐在Spring Service方法上加@Transactional(rollbackFor = Exception.class),并且库存扣减使用乐观锁或条件更新:

boolean updated = productMapper.update(null, new LambdaUpdateWrapper<Product>() .eq(Product::getId, productId) .ge(Product::getStock, quantity) .setSql("stock = stock - {0}", quantity) ) > 0;

这样即使多个用户同时下单,库存也不会扣成负数。需要注意的是,如果后续要做高并发秒杀,这个乐观锁方案会大量更新失败,需要引入Redis缓存库存和分布式锁;但对于中小型旗袍商城,数据库条件更新已经足够了。

订单号也是容易被忽略的细节。直接用数据库自增id当订单号会暴露真实销量,建议生成规则为:时间戳(yyyyMMddHHmmss)+ 随机数 + 用户id后四位。虽然看起来丑,但可读性和唯一性都能兼顾。

4. Vue前端实战:页面路由、状态管理与接口联调

4.1 前端工程和路由设计

前端部分我使用Vite创建Vue3工程,比Vue CLI启动速度快很多。目录结构大概是:

  • views/user:用户端页面
  • views/admin:后台管理页面
  • router:路由配置
  • store:Pinia状态管理
  • api:接口封装
  • utils:axios实例和工具函数

路由配置里最重要的两个点:懒加载和登录守卫。商城首页如果一次加载全部页面,首屏会非常慢,所以路由必须用动态import。登录守卫则是在进入购物车、订单结算、个人中心等页面时,判断本地是否有token,没有就跳转登录页。

const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: () => import('@/views/user/Home.vue'), meta: { title: '首页' } }, { path: '/cart', component: () => import('@/views/user/Cart.vue'), meta: { requiresAuth: true } } ] }) router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else { next() } })

实际开发中还有个小技巧:用Pinia保存用户信息时,不要只存token,还要在刷新页面时再次调用/user/info接口拉取最新用户数据。否则用户改了头像或昵称,前端很多页面还是旧数据。

4.2 Axios封装与接口联调细节

一个项目里最怕每个页面自己写一套请求逻辑,万一后端改了响应结构,改起来会疯掉。所以axios实例一定要统一封装。

封装时需要注意的点不少:请求头自动加token,响应时统一判断code字段,如果code为401就清空本地登录信息并跳转登录页,如果code不为200就弹出错误提示。还不能忘了错误处理拦截器。

const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( res => { const data = res.data if (data.code === 200) { return data } if (data.code === 401) { router.push('/login') return Promise.reject(data) } return Promise.reject(data) }, err => { return Promise.reject(err) } )

接口联调阶段最常见的是跨域问题。我在本地开发时用Vite的proxy代理解决,前端请求写/api,后端接口实际是http://localhost:8080/api,这样浏览器不直接跨域,联调体验会顺畅很多。

// vite.config.js export default defineConfig({ server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } } })

这里有个容易踩的坑:如果后端网关是/api开头,那么前端proxy的rewrite可能要写成不替换;如果后端没有/api前缀,才需要rewrite掉。先和后端确定好统一前缀,否则接口全是404。

4.3 首页、商品详情和购物车页面的实现要点

首页最核心的模块是商品瀑布流和分类导航。商品列表建议后端返回分页数据,前端用“加载更多”而不是一次性加载全部。旗袍商品图是重点,图片加载一定要用懒加载,否则首屏图片过多会卡。

商品详情页是转化率最高的页面,我做了三块内容:

  • 商品主图轮播和缩略图切换
  • 可选的定制属性区,包括领型、袖长、盘扣、绣字等
  • 价格动态计算区

属性区交互我维护一个selectedAttrs对象,用户点击属性时更新它,同时计算额外价格,最终价格由基础价加额外价组成。这个交互考虑到定制属性组合很多,不能每次都请求后端,所以价格计算放在前端完成,后端在下单时再校验一次金额合理性。

购物车页面我建议把“选中状态”和“数量”都放在本地Pinia里,但每次变化后都要调用后端接口同步。有人可能会嫌频繁,于是只在点击“结算”时统一提交,但购物车图标上的角标数字就会不准确。折中办法是用防抖函数,在用户停止操作后300毫秒再同步一次,既减少了请求也不会有明显延迟。

5. 上线部署与性能优化:从打包装到Nginx落地的完整路径

5.1 后端打包、前端构建与Nginx配置

在部署阶段,后端先通过Maven打包成可执行Jar包。打包前要把application.yml里的数据库地址改成生产环境数据库,如果使用Nacos或Apollo等配置中心,可以跳过本地配置,但我个人做中小型项目时为了方便,还是坚持用环境变量区分配置。

启动命令建议写成:

java -jar qipao-mall-server.jar --spring.profiles.active=prod

前端构建就比较简单了:

npm run build

构建完成后会生成dist目录,把它放到Nginx的html目录下,再配置反向代理将/api请求转发到后端端口。

server { listen 80; server_name qipao.example.com; root /data/www/qipao/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; 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 $uri $uri/ /index.html;,如果没有这一行,用户刷新详情页会出现404,因为Vue Router用的是history模式,Nginx找不到对应的物理文件。第一次部署时我在这里卡了很久。

5.2 图片资源优化和缓存策略

旗袍商城全是服装图片,一张原图动辄几MB,如果直接用原图,页面加载会让人崩溃。我在项目中做了三件事:

第一,后端在上传时用Thumbnailator生成三套尺寸,分别是列表小图300px、详情中图800px、原图1600px。前端列表页用300px,详情轮播用800px,放大镜像用原图,这样流量能省下一大半。

第二,图片统一走独立路径,比如/static/upload/,Nginx给这个路径设置长缓存:

location /static/ { expires 30d; add_header Cache-Control "public"; }

第三,如果后续用户量和图片量上来了,把图片迁移到对象存储。数据库里存的图片地址不要写相对路径,最好直接存完整URL前缀,或者存/static/upload/2024/xxxx.jpg这样的相对路径,迁移时只要在对象存储域名前拼接即可。

5.3 常见问题速查表

问题现象可能原因解决办法
前端请求接口返回404Vite proxy的rewrite配置错误,或后端接口路径不匹配确认/api前缀是否需要替换,检查后端Controller的RequestMapping
登录成功后刷新页面又跳到登录页没有在App初始化时调用用户信息接口,或token没有持久化在路由守卫里判断token存在时拉取用户信息,Pinia重新赋值
商品列表查询很慢分类表关联查询没有索引,或数据量大后没有分页给category_id、status、deleted加复合索引,强制走分页
下单时库存变成负数扣减库存使用普通update,没有条件判断用where stock >= quantity的条件更新,并加事务
上传图片后访问图片404上传目录不在Nginx托管路径下,或没有创建目录确保上传目录存在,Nginx配置静态路径指向该目录
后台管理页面白屏打包后访问history路由没有重定向到index.html检查Nginx的try_files配置
用户下单重复提交点击结算按钮没有做防止重复提交前端按钮loading禁用,后端幂等控制或防重token

6. 垂直商城复用扩展与操作心得

6.1 从旗袍商城扩展到其他垂直品类的思路

我在做这个系统时,最大的感受是垂直电商不一定要重做一套系统。旗袍商城的核心结构,商品、订单、用户、购物车,和茶叶、文玩、手工皮具这类非标品几乎是一致的。差异主要在于商品属性模型和订单履约流程。

如果你需要把这个系统复用到其他品类,建议把product_attr抽象成独立属性定义表和属性值表。比如旗袍有“领型”、“袖长”,茶叶有“产地”、“年份”,虽然名称不一样,但数据模型都可以归为“属性名+属性值+额外价格”。界面上的自定义表单,也可以通过动态渲染属性配置来生成,而不是每接一个品类就改一版前端。

定制流程通常还会涉及“量体信息”、“工期说明”、“预约上门”这些字段。这些适合另外建一张extend_info表,主键关联订单ID,前台展示时动态拼接。这样商品主表始终保持简洁,扩展定制字段不影响核心订单流程。

6.2 几个值得坚持的小习惯

我在实际开发中慢慢总结出几条经验,虽然不复杂,但确实能减少返工:

  • 前后端接口返回结构一定要统一,我统一用{code:200, data:{}, msg:"success"},这样前端封装一次请求逻辑就够了。
  • 所有金额字段用Decimal存,不要在Java里用double计算价格,不然会有精度问题,展示时出现分位错误很难看。
  • 订单状态枚举要写在代码里,不要到处用魔法数字。比如0待支付、1待发货、2待收货、3已完成、4已取消,单独建一个常量类或者枚举类。
  • 日志里不要只打印异常信息,要打印订单号、用户ID这些上下文,否则出问题排查日志时非常痛苦。

最后再分享一个小技巧:做这类商城系统,先把购物车和订单的边界梳理清楚再写代码。购物车是临时状态,可以随时改;订单是事实记录,一旦生成就不能随便改,所有商品信息都要做快照。这个原则理解透了,后面开发会顺很多。旗袍商城不是一个大而全的电商平台,但它刚好涵盖了电商系统的绝大部分核心链路,做一遍下来,Spring Boot、Vue、数据库设计、部署运维这些基本功都能得到很扎实的锻炼。

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

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

立即咨询