news 2026/10/18 11:46:54

SpringBoot实战:从零搭建“衣依”服装销售平台全流程解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot实战:从零搭建“衣依”服装销售平台全流程解析

从零搭一个能用的服装销售平台,这事儿说难不难,但要说省心,那也是骗人的。

“衣依”这个项目,是我用 SpringBoot 完整走完的一套服装销售平台,从需求梳理、数据库建模、后端接口,到页面渲染、模拟支付、部署上线,最后还配了一篇像模像样的说明文档。如果你正准备做类似的 Java Web 项目,或者正愁毕设选题不知道怎么落地,那这篇东西就是给你写的。我会把整个设计过程、核心代码思路、还有我实际踩过的坑,全部摊开来讲,你照着走一遍,基本就能摸清这类项目的完整套路。

1. 先把需求盘明白:服装销售平台到底要做什么

1.1 为什么选服装销售平台而不是通用电商

很多人一上来就想做“电商系统”,结果做着做着发现功能越加越多,购物车、优惠券、秒杀、直播带货全都想要,最后要么草草收场,要么代码烂成一锅粥。我的建议是,选题一定收敛到具体领域。服装销售平台的好处在于:它有明确的行业属性,商品有分类(男装、女装、童装)、有规格(尺码、颜色)、有库存概念,比单纯卖书卖数码更贴合真实业务,但又不至于像全品类电商那样复杂到失控。

“衣依”这个名字也是当时随手起的,寓意“衣”字打头,做服装生意嘛,顺口好记就行。项目最终定位是:面向消费者的服装购买平台 + 面向管理员的商品运营后台,用户能浏览商品、加入购物车、下单支付、查看订单;管理员能管理商品分类、上下架商品、处理订单状态。

1.2 用户端和管理端的边界划分

我见过很多新手项目,把用户端和管理端混在一个 Controller 里,一个方法里面既查用户数据又改管理员配置,最后权限完全失控。所以动手写代码前,先把边界画清楚:

  • 用户端:注册、登录、浏览商品、查看商品详情、管理购物车、提交订单、模拟支付、查看/取消订单、维护收货地址。
  • 管理端:管理员登录、商品分类管理、商品信息管理(上下架、改价、改库存)、订单列表处理(发货、完成)、简单的数据统计。

两边的共性只有“用户体系”和“商品体系”这两块底层数据,其余逻辑完全独立。这样分工清晰,后续写 Controller 的时候也不容易乱。

1.3 技术选型:不追新,只求稳

这个项目的技术栈非常经典,我强烈建议第一次做完整项目的人不要搞花活,就用这套:

  • Spring Boot 2.7.x:主流版本,资料多,遇到问题一搜就有答案;
  • MyBatis Plus:单表 CRUD 基本上不用写 SQL,专注业务逻辑;
  • MySQL 8.0:存储业务数据;
  • Thymeleaf:服务端渲染页面,不搞前后端分离;
  • Bootstrap 5 + 少量 jQuery:快速搭建界面,不自己写 CSS 框架;
  • Maven 管理依赖,Lombok 简化实体类代码。

为什么不选 Vue 前后端分离?因为对于单人或双人完成的项目来说,前后端分离意味着要维护两套工程、处理跨域、还要额外部署前端静态资源,工作量直接翻倍。服务端渲染虽然“土”,但是一个 SpringBoot 工程搞定所有事情,跑起来就是一个整的系统,出问题也好排查。

提示:毕设答辩时老师问“为什么用 Thymeleaf”,你可以理直气壮地说:为了降低系统复杂度,让部署更简单,同时保证 SEO 友好。这就是实话。

2. 数据库建模是整栋楼的承重墙,别急着写代码

2.1 核心表结构:从用户到订单的闭环

我建表的时候一共设计了 9 张表,分别是:用户表、管理员表、商品分类表、商品表、购物车表、订单表、订单明细表、收货地址表、轮播图表。其中最关键的关系,是订单表与订单明细表的“一对多”关系。

CREATE TABLE `orders` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `user_id` bigint(20) NOT NULL COMMENT '下单用户ID', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待付款 1已付款 2已发货 3已完成 4已取消', `address_snapshot` varchar(500) DEFAULT NULL COMMENT '收货地址快照', `create_time` datetime NOT NULL, `pay_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

订单明细表也很关键,它记录每一件商品的下单时刻信息:

CREATE TABLE `order_item` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_id` bigint(20) NOT NULL, `product_id` bigint(20) NOT NULL, `product_name` varchar(100) NOT NULL COMMENT '下单时的商品名快照', `product_image` varchar(255) DEFAULT NULL, `price` decimal(10,2) NOT NULL COMMENT '下单时的单价快照', `quantity` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2.2 订单快照 vs 实时关联查询:这一个决定我少改了很多 bug

刚开始我把订单明细设计成“下单时只存 product_id,查订单详情的时候再去 join product 表拿名称和图片”。后来测试的时候发现一个大问题:如果管理员把商品下架了,甚至删除了,用户历史订单里的商品信息全变成空白。这是电商系统里最典型的错误。

正确的做法就是订单明细表里冗余商品名称、图片、单价快照。用户下单那一刻,价格是多少就存多少,商品叫什么就存什么,以后再改商品表里的信息,完全不影响历史订单。同理,订单表里也存了收货地址快照,而不是只存一个 address_id。这就是“快照”设计思路,你记下来,以后做任何交易类系统都用得上。

2.3 库存扣减:我选择下单时扣,不选择支付时扣

库存扣减有两种常见的时机:加入购物车时扣、提交订单时扣或者支付成功时扣。我最终选择了“提交订单时扣减并锁定库存,支付超时自动释放”。具体是:用户点击“提交订单”,系统先检查库存,如果够,就扣减库存并创建待支付订单;如果用户一直不支付,超过 30 分钟,订单自动取消并回补库存。

为什么不选择支付成功后再扣库存?因为这样子会导致“下单成功但支付时发现没货”的尴尬局面,用户体验很差。而边下单边扣库存的方式,只要库存扣成功,订单就一定成立,后面支付只是完成交易动作。

注意:真实高并发场景里这个方案还不够,要引入 Redis 分布式锁或者消息队列异步扣减。但单机项目里,只要把“扣库存”和“创建订单”放在一个数据库事务里,就已经足够安全了。

3. 后端实现细节:从用户注册到订单支付的全链路

3.1 注册登录:密码不能明文存,Session 管理别偷懒

用户模块看似简单,但很多项目都挂在密码上。我在项目里使用了 Spring 自带的 BCryptPasswordEncoder 对密码做哈希存储,绝对不会用明文。注册的逻辑就是先查重邮箱或用户名,再加密入库;登录就是比对哈希值,成功后把用户信息放进 Session。

拦截器这个点我要重点提一下。用户端很多接口(购物车、下单、订单列表)必须登录后才能访问,管理员端更是如此。我当时写了一个 WebMvcConfigurer 注册两个拦截器:

@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserLoginInterceptor()) .addPathPatterns("/cart/**", "/order/**", "/user/**") .excludePathPatterns("/user/login", "/user/register", "/static/**"); registry.addInterceptor(new AdminLoginInterceptor()) .addPathPatterns("/admin/**") .excludePathPatterns("/admin/login", "/static/**"); }

用拦截器统一处理登录校验,比在每个 Controller 里手动判断 Session 要干净得多。这里有个小坑:Thymeleaf 模板渲染静态资源时,路径要记得排除/static/**,否则页面加载不出 CSS 和 JS。

3.2 商品列表与分页:MyBatis Plus 的 IPage 用起来非常顺手

商品列表是用户浏览的主入口。我在前端做了一个分类导航栏,点击不同分类就请求对应该分类下的商品,同时支持按价格和上架时间排序。后端查询直接用了 MyBatis Plus 的分页插件:

public PageResult<ProductVO> getProductPage(long categoryId, int pageNum, int pageSize, String sort) { Page<Product> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); if (categoryId > 0) { wrapper.eq(Product::getCategoryId, categoryId); } wrapper.eq(Product::getStatus, 1); // 只查上架商品 if ("price_desc".equals(sort)) { wrapper.orderByDesc(Product::getPrice); } else if ("price_asc".equals(sort)) { wrapper.orderByAsc(Product::getPrice); } else { wrapper.orderByDesc(Product::getCreateTime); } productMapper.selectPage(page, wrapper); // 组装商品名称、图片、价格信息返回前端 }

这里有一个很影响体验的细节:商品列表不能把下架商品查出来给用户看。所以查询条件里务必加上status = 1。很多新手忘了这个过滤条件,导致用户在下单时才发现商品已经下架,白白流失一波流量。

3.3 购物车与订单生成:事务保证一致性

购物车表很直接,就五个字段:id、user_id、product_id、quantity、create_time。用户加购时,我先查一下购物车里有没有同样商品,有就数量加一,没有就新插入一条。这里要提醒一下:加购操作要考虑商品是否上架、库存是否足够,否则用户满怀期待加购,结算时才发现没货,体验很差。

订单生成是整个系统最需要谨慎的地方。我把它设计在一个事务方法里:先生成订单主记录,然后遍历购物车中的商品,逐条生成订单明细,扣减库存,最后清空购物车。如果中间任何一步失败了,数据库回滚,保证不会出现“订单生成了但库存没扣”或者“库存扣了但订单明细缺失”的情况。

@Transactional(rollbackFor = Exception.class) public OrderVO createOrder(Long userId, Long addressId) { // 1. 查询购物车列表 // 2. 校验商品是否上架、库存是否充足 // 3. 计算总金额 // 4. 插入订单记录和订单明细 // 5. 批量扣减库存 // 6. 清空购物车 // 7. 返回订单号 }

3.4 模拟支付:别真的去接支付宝,但状态流转要逼真

毕设项目肯定不能真的接入真正的支付网关,除非你打算完成企业级真实支付。所以这里我用了一个“模拟支付”的方式:用户点击“立即支付”,弹出一个确认支付页面,展示订单号和金额,点击“确认支付”后,后端直接把订单状态从“待付款”改为“已付款”,并记录支付时间。

但状态流转的严谨性不能丢。我定义了一组订单状态常量:0-待付款、1-已付款、2-已发货、3-已完成、4-已取消。订单生成的初始状态是 0,模拟支付后是 1,管理员发货后是 2,用户确认收货后是 3。取消订单分为用户主动取消和超时自动取消,取消时都要回补库存。

超时自动取消我用的是最简单的方案:在订单详情页请求的时候,检查创建时间是否超过 30 分钟,如果超过且状态仍是待付款,就自动置为已取消并回补库存。这个方法不精准,也没有定时任务,但对单机毕设项目来说完全够用,而且答辨起来也好解释。

4. 页面从哪来:用 Thymeleaf 组织前台和后台

4.1 前台的页面骨架与公共片段抽取

Thymeleaf 最大的优势就是可以直接在 HTML 里使用th:each、th:if这些属性去渲染动态数据,配合th:fragment抽取公共片段,就不用每个页面重复写头部导航栏和底部版权信息了。

我当时的页面结构是这个样子:

templates/ ├── layouts/ │ ├── user-header.html(用户端导航栏) │ └── admin-sidebar.html(管理端侧边栏) ├── user/ │ ├── index.html(首页) │ ├── list.html(商品列表) │ ├── detail.html(商品详情) │ ├── cart.html(购物车) │ ├── confirm.html(订单确认页) │ ├── pay.html(模拟支付页) │ └── order-list.html(我的订单) └── admin/ ├── login.html(管理员登录) ├── dashboard.html(控制台) ├── category.html(分类管理) ├── product.html(商品管理) └── order.html(订单管理)

首页其实是整个系统颜值的门面,我用 Bootstrap 的轮播组件做 Banner 展示,下面再按分类展示四到六个推荐商品卡片。商品列表页用卡片式布局,每个商品显示图片、名称、价格和一个小按钮。整个过程没有自己写多少 CSS,完全依赖 Bootstrap。

4.2 表单提交回显与参数校验:后端校验不能省

页面上的表单(注册、登录、添加商品、编辑商品)都要处理一个问题:用户提交的数据不合法,报错了怎么办?如果直接弹回一个错误页面,体验很糟糕。正确的做法是后端校验失败后返回原表单页面,并且把用户之前输入的内容回显出来,让他修改而不是重新填。

Spring Boot 里我用的是@Validated注解配合 BindingResult 来做参数校验,比如用户名非空、价格大于零、库存不能为负这些基础规则。同时,Controller 里不要直接接受实体类,而是用专门的 VO 或 DTO 接收前端参数,这样既能控制字段边界,又能做校验,不会把数据库字段全暴露到前端。

提示:关于参数接收,我一贯的原则是:前端传什么就定义什么,不要偷懒直接拿实体类接收。比如新增商品时,用户不可能传 create_time 字段,但你如果用 Product 实体接收,就等于给了别人一个绕过逻辑直接造数据的机会。

4.3 商品图片存储:本地目录 + 虚拟映射,别把图片塞进数据库

商品图片怎么存,也是新手容易踩坑的地方。我采用的方案是:图片文件上传到服务器本地目录/upload/,数据库里只存相对路径,然后通过 Spring Boot 的虚拟资源映射,把/upload/**映射到本地磁盘路径。

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

这样做的好处是页面直接<img th:src="${product.image}">就能访问图片,不需要额外的图片服务。部署到服务器时,只要把 upload 目录一起拷贝走就行。图片名我用 UUID 重新生成,避免中文名和重复名导致的问题。

5. 实测中的拦路虎:这些坑我一个个替你蹚过了

5.1 日期格式化:LocalDateTime 返回给前端时变成了一长串数字

这是我调试时最先发现的问题。数据库里存的是 datetime,实体类用的是 LocalDateTime,结果接口返回给前端时,日期字段变成类似2025-01-15T10:30:00这样的格式,甚至有时候直接显示成时间戳数字,页面根本没法看。

解决方式是在 application.yml 里配置全局日期格式:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

同时,实体类上的日期字段可以加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")做兜底。Thymeleaf 模板里也可以用#temporals.format(order.createTime, 'yyyy-MM-dd HH:mm:ss')来控制展示格式。三种方式任选其一,别全写上去,否则反而乱。

5.2 库存并发:两个人同时买最后一件衣服,系统崩溃了

我最初测试时只有自己一个账号,点击下单后库存完全够用,根本没发现并发问题。后来开了两个浏览器窗口同时下单同一件库存只剩 1 件的商品,结果两个订单都创建成功了,库存变成了 -1。这就是典型的超卖问题。

修复方案是在扣库存的 SQL(或者 Mapper 方法)上加一个条件:UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0。这样一来,数据库层面就能保证扣减操作是原子的,两个事务同时执行时只有一个能成功,另一个影响的记录数为 0,就可以抛出“库存不足”的异常。

5.3 部署时端口与数据库配置:本地好好的,服务器上全乱套

在本地开发时,数据源配置直接写死 localhost,端口默认 8080,一切正常。但打包部署到云服务器时,这些问题全冒出来了:MySQL 是远程的,密码不一样;端口被占用;上传目录路径变了。

我最后的做法是把公共配置放到application.yml,把环境相关的配置放到application-prod.yml,打包时通过启动参数指定环境:

java -jar yiyi-shop-0.0.1.jar --spring.profiles.active=prod

同时,配置里全部使用环境变量占位符,比如${DB_HOST}、${DB_PASSWORD},在服务器上用 export 设置好,下次部署就不用改代码了。这个经验非常实用,不管什么项目,环境配置从第一天开始就分离好,能省下大量时间。

6. 这套代码怎么变成一份体面的项目文档

6.1 归档源码:包结构要干净,注释要像人话

源码交付出去之前,我花了整整一个下午去清理包结构和注释。不夸张地讲,一个包结构混乱的项目,不管功能多好,给人的第一印象都是大打折扣。我的包结构是这样组织的:

com.yiyi.shop ├── controller(用户端 + 管理端分目录存放) ├── service(接口 + impl 实现类) ├── mapper(MyBatis Plus 的 Mapper 接口) ├── entity(数据库实体类) ├── vo(给前端展示用的值对象) ├── dto(接收前端参数的传输对象) ├── config(配置类,如拦截器、资源映射、MyBatis Plus 配置) └── common(统一返回结果、常量类、异常处理)

注释我控制在“每类注释一次,不逐行注释”的粒度。类上面的注释写清楚这个类负责什么,方法上的注释写清楚参数含义和返回值,业务关键节点写一行原因说明(比如“这里用快照防止商品信息变更影响历史订单”)。彻底删掉那些// 获取订单列表废话注释,那只会让人觉得代码写得不清楚。

6.2 文档结构:设计说明书的正确打开姿势

配套文档不一定要写成八股文,但基本骨架必须有:项目背景与意义、需求分析、系统设计(架构图 + 功能模块 + 数据库设计)、核心功能实现、系统测试、总结与展望。

我在数据库设计部分花了最多篇幅,每张表都配了字段说明表,还在关键表之间画了关系说明(用文字描述,不用复杂的 ER 图工具)。核心功能实现部分,我只挑下单和支付两个最有含金量的流程,贴关键代码片段,配上文字说明这个流程是怎么跑的、为什么这么设计。切忌把整个 Controller 全部贴进论文,没人看。

6.3 答辨预案:老师最爱问的几个问题,先准备好答案

答辨的时候,与其被动挨问,不如提前把所有“被问穷”的点都自查一遍。我遇到的最高频问题如下:

  • “为什么用 SpringBoot 而不用 SSM?”答:SpringBoot 自动配置简化了项目搭建,内嵌 Tomcat 让部署变简单,生态成熟适合快速开发。
  • “订单表为什么要存地址快照?”答:保证历史订单不随基础数据变更而失真,是电商领域常见的冗余设计。
  • “库存扣减怎么防止超卖?”答:使用条件更新UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?,数据库层面保证原子性。
  • “前后台共用一个后端吗?”答:共用一套 SpringBoot 服务,通过拦截器和路径前缀区分用户端与管理端权限。

这四条答得顺畅,答辨基本不会难堪。剩下的问题基本都是细节,按自己项目的实际代码如实回答就行。

写在最后

我从一个查数据库的“菜鸟新手”到能独立跑通这个“衣依”服装销售平台,最大的体会是:做项目这件事,动手是第一位的,方案再好也要落到代码里验证,踩了坑才能真正理解为什么要那样设计。白纸黑字的表结构、事务注解、快照字段,这些在课件上都是零碎知识点,但当它们共同支撑起一个能运行、能下单、能发货的系统时,知识就变成了经验。

如果你正准备开工类似项目,我的建议是先花一个晚上把表结构定清楚,把订单状态机的流转画明白,再动笔写代码,至少能帮你省下两天的返工时间。等你跑通一单“从注册到支付到发货”的完整流程,你就能底气十足地说:这个系统,是我的作品。

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

Obsidian dataview插件从安装到排错:让笔记查询动起来

简介&#xff1a;Obsidian Dataview 插件资源包&#xff0c;面向使用 Obsidian 管理个人知识库的用户&#xff0c;以及希望借助查询语言自动化整理笔记的进阶学习者。插件支持基于日期表达式的倒计时提醒、Markdown 表格的动态创建与筛选&#xff0c;以及跨笔记的任务聚合查询&…

作者头像 李华
网站建设 2026/10/18 11:46:31

从模糊标题到技术方案:以rea为例的逆向拆解与排查方法论

1. 当标题只剩下三个字母&#xff1a;一次关于“rea”的逆向拆解拿到“rea”这个标题的时候&#xff0c;我第一反应是&#xff1a;这要么是个缩写&#xff0c;要么是个半截词&#xff0c;要么就是某个内部项目的代号。正文是空的&#xff0c;关键词是空的&#xff0c;摘要也是空…

作者头像 李华
网站建设 2026/10/18 11:45:57

GooseFS元数据发现:让缓存服务主动感知底层存储

最近 GooseFS 社区发布了元数据发现功能&#xff0c;这个动作看着不大&#xff0c;但实际意义可能比大多数人想的要深。它把缓存服务从「被动等请求」往前推了一大步&#xff0c;开始主动感知底层存储里的数据长什么样。对于跑过分布式训练、做过大数据平台加速、被海量小文件拖…

作者头像 李华
网站建设 2026/10/18 11:44:21

基于Spring Boot的高校毕业生实习管理系统核心设计解析

简介&#xff1a;基于Java的高校毕业生实习管理系统毕业设计资料包&#xff0c;面向计算机相关专业毕业生与Java Web开发者&#xff0c;借助信息化手段解决高校实习管理中岗位发布分散、学生申请流程冗长、成绩统计困难等痛点&#xff0c;覆盖用户登录注册、实习岗位发布、学生…

作者头像 李华
网站建设 2026/10/18 11:41:50

Attention-GRU单维时序预测实战:原理、代码与调参详解

做时间序列预测的人迟早都会遇到这种需求&#xff1a;手头只有一列数值&#xff0c;没有外部特征&#xff0c;要预测未来几个时间步的走势。比如某个站点过去两年的用电负荷、某条管道的水位、某个业务的日活跃量&#xff0c;这种问题在行业里统一叫单维时序预测。我最初拿这类…

作者头像 李华
网站建设 2026/10/18 11:41:40

Java课设学生成绩管理系统:从三件套到JDBC排错与批量导入

简介&#xff1a;一套面向初学JavaWeb开发者的学生成绩管理系统实战项目&#xff0c;旨在帮助读者把Servlet、JSP、模型-视图-控制器设计模式与动态代理等知识串联起来&#xff0c;落实到登录认证、学生管理、课程管理、成绩录入等完整业务流程中。压缩包内有271个文件&#xf…

作者头像 李华