资讯动态

Spring Boot时装购物系统毕设实战:从建表到部署完整指南

发布时间:2026/10/27 15:13:17 来源:尧图企业网站定制
简介面向Java毕业设计开发的基于Springboot网页时装购物系统配套完整论文、源码、开题报告与答辩PPT。系统采用B/S架构以Java与Springboot框架为核心结合MySQL数据库完成前台时装展示、用户管理、订单处理等功能覆盖从需求分析、系统设计到系统测试的完整流程适合计算机相关专业学生用于毕业设计参考或二次开发。资源包约35.57MB内含论文、开题报告、答辩PPT与项目源码等主要文件文档章节涵盖绪论、相关技术、系统分析、系统设计、系统详细设计与系统测试具体包括技术可行性、经济可行性、操作流程、添加信息流程、删除信息流程、数据库设计原则、数据表结构以及管理员与用户功能模块等内容完整便于直接对照学习。答辩PPT与开题报告可用于展示与汇报源码则能帮助理解具体业务实现。目前已有45人学习下载对于需要快速完成Springboot类购物系统毕设的同学可有效借鉴其模块划分、数据库模型、核心代码逻辑与测试方法缩短开发与论文撰写周期。1. 从毕设选题到可运行系统这个时装购物项目到底要交付什么如果你正在找毕业设计选题或者手里已经拿到“基于Springboot的网页时装购物系统”这个题目但不知道从哪下手这篇文章就是冲着你来的。这类项目在本科毕设里出现频率极高核心诉求很直接用 Spring Boot 把一个带前端页面、后台管理、数据库交互的购物网站完整做出来能演示、能答辩、代码能讲清楚。它不是科研项目不需要你有分布式、高并发的架构能力但它要求你把 MVC 分层、CRUD、会话管理、事务处理这些基本功走一遍并且走得完整。时装购物系统相比普通图书或商品管理系统多了一层“展示”压力。服装讲究品类、季节、尺码、颜色、图片效果前端浏览体验会直接影响答辩时老师的第一印象。很多人做着做着就变成了一个空壳的增删改查商品表和订单表各管各的购物车只是摆设下单没有任何事务控制。这样的系统跑起来容易但答辩现场一问“订单和库存怎么保持一致”就露馅。这篇实战笔记会把从建表到部署的完整路径拆开讲连版本选择、静态资源放行、金额精度这类容易翻车的细节也会单独拎出来说。适合看这篇文章的人有两种一种是你准备复现或改造一套毕设源码需要知道项目结构和核心代码怎么组织另一种是你打算从零自己写需要一个能落地、能讲得通的方案。下面的内容不依赖任何特定开源包所有代码都可以直接敲进你自己的工程里跑通。2. Spring Boot 项目怎么搭分层结构与前后端落地选型2.1 为什么是 Spring Boot 而不是 Spring MVC 或 SSM做毕设的人最容易纠结的第一步就是技术选型。Spring Boot 之所以在这类题目里几乎成了默认答案是因为它把配置工作压到了最低。传统 SSM 要写 web.xml、Spring 配置文件、MyBatis 映射文件扫描路径光是让项目跑起来可能就消耗一到两天Spring Boot 通过 starter 机制把常用组件的自动配置封装好了内嵌 Tomcat 也让部署变成了一个 jar 直接启动。对于时装购物系统这种业务复杂度不高的 Web 应用Spring Boot 单工程就能覆盖全部需求不需要拆微服务。另一个重要的点是答辩现场的可解释性。Spring Boot 的约定大于配置目录结构一眼能看出 controller、service、mapper 的职责边界评委问“你的事务写在哪一层”“你的拦截器怎么生效”你可以直接指出对应类和注解不需要绕弯子解释一堆 XML 里的 bean 定义。如果之前学过 SSM也不要觉得精力白费Spring Boot 底层还是 Spring 那套东西你会的 Bean 生命周期、AOP、依赖注入在这里全都能用上。依赖管理上我建议用 Maven。虽然 Gradle 在热词里出现的频率不低但大部分毕设源码和答辩环境还是 Maven 更通用阿里云镜像拉包也方便。项目骨架可以手动建也可以用 Spring Initializr 生成关键依赖就四个spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java、lombok。再加一个 Thymeleaf 还是用前后端分离取决于你前端页面想怎么做下面单独说。2.2 页面怎么做Thymeleaf 与 Vue 打包进 Spring Boot 的选择时装购物系统要展示图片、颜色、尺码这类商品属性页面美观度很重要。这里通常有两条路。一条是用 Thymeleaf 做服务端渲染页面是放在 Spring Boot 工程里的 HTML 模板通过 ModelAndView 返回数据渲染另一条是先把 Vue 项目打包把 dist 目录拷进 Spring Boot 的 static 目录前端调用后端接口获取 JSON 数据。两条路都能在答辩时跑通但代码组织和排错方式完全不同。我的建议是如果你对 JavaScript 和前端工程化不熟老老实实用 Thymeleaf。因为它在同一个工程里不需要处理跨域不需要维护前后端两个项目接口报错直接在 controller 里追。如果你已经会 Vue 的基本用法那就用前后端分离再打包合一的方案页面表现力强得多。这里的关键操作是 Vue 打包后把 dist 内容复制到 src/main/resources/static 下然后后端不写任何视图跳转只写 RestController 返回数据。以下是接通前后端的核心配置放在 application.yml 里。server: port: 8080 servlet: context-path: / spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/fashion_shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 50MB mvc: static-path-pattern: /** mybatis-plus: mapper-locations: classpath*:mapper/*.xml type-aliases-package: com.fashion.shop.entity configuration: map-underscore-to-camel-case: true这里有几个参数值得注意。serverTimezoneAsia/Shanghai 是必须写的MySQL 8.x 默认时区跟中国本地有偏差不写会在时间字段存取时差八小时。static-path-pattern 用 /** 表示所有非接口路径都去找静态资源Vue 打包出来的 index.html 就是一个普通的静态文件。map-underscore-to-camel-case 让数据库的下划线字段自动映射成实体类的驼峰属性这是 MyBatis-Plus 的常用配置。文件上传大小限制要提前放开时装商品图片经常是几 MB 的手机拍摄图默认 1MB 会直接报错。2.3 项目目录结构与职责边界不管用哪种前端方案后端目录结构都可以统一成下面这个样子。它是标准 Spring Boot 分层controller 接参数、service 写业务、mapper 管 SQL、entity 对应表结构外加 config 和 common 两个辅助包。很多人做毕设翻车是因为把业务逻辑写在 controller 里遇到订单和库存联动的时候代码变成一坨答辩想讲也讲不清。src/main/java/com/fashion/shop ├── controller # 接收前端请求返回 JSON 或页面 │ ├── UserController.java │ ├── GoodsController.java │ ├── CartController.java │ └── OrderController.java ├── service # 业务逻辑层事务注解加在这里 │ ├── UserService.java │ ├── GoodsService.java │ ├── CartService.java │ ├── OrderService.java │ └── impl/ ├── mapper # 数据访问层继承 BaseMapper │ ├── UserMapper.java │ ├── GoodsMapper.java │ ├── CartMapper.java │ └── OrderMapper.java ├── entity # 实体类字段与表结构对应 ├── config # 拦截器、跨域、静态资源拦截放行 ├── common # 统一返回结果封装比如 RT └── ShopApplication.javaservice 包下面的 impl 子包是接口实现类这是最常见的 Java 惯例写法。统一返回结果封装 R 我觉得是毕设里性价比最高的小工具它让所有接口返回格式变成 {code, message, data}前端判断 code 是否为 200 就知道请求有没有成功拦截器里的异常处理也统一走这一个出口。很多源码示例习惯直接返回 Map 或裸对象短期看省事但答辩时老师问“你的接口错误怎么统一处理”你就只能支支吾吾。3. 时装购物系统的数据库设计五张核心表与字段取舍3.1 从业务逆推表结构用户、商品、购物车、订单、订单明细数据库设计是整个系统里最不应该省时间的部分。很多人一上来就建表建到哪算哪到写订单功能时发现没有订单明细表又把表推倒重来。时装购物系统的核心链路是用户浏览商品、加入购物车、提交订单、管理员处理订单。这条链路至少需要五张表用户表 user、商品表 goods、购物车表 cart、订单表 orders、订单明细表 order_item。购物车表和订单明细表看起来都是记录“哪个用户买了哪个商品”但职责完全不同。购物车是临时状态用户可以反复增删改不需要留历史订单生成后购物车里的记录应该被清掉订单明细是交易快照商品名称、价格、尺码、数量在下单那一刻就要固定下来之后商品表的价格变化不能影响已生成的订单。很多初写者用一张表同时干这两件事结果改购物车的时候把订单也污染了。所以陈年经验就是临时状态和持久状态必须分表。订单表用 orders 而不是 order因为 order 是 SQL 关键字直接建表会语法报错虽然加反引号能绕过去但没必要给自己埋雷。订单状态字段建议用 tinyint 表示0 待支付、1 已支付、2 已发货、3 已完成、4 已取消。不要用字符串存“待支付”“已支付”这样的中文值排序和统计都会变麻烦查询状态时还要做字符匹配。3.2 关键建表 SQL 与字段说明现在把核心几张表的建表语句贴出来你可以直接复制到 Navicat 或命令行执行。字段类型的选择已经包含了踩坑后调优的结果比如价格用 decimal 而不是 double原因后面细说。CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT BCrypt 加密后的密码, nickname VARCHAR(50) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像图片路径, role TINYINT DEFAULT 0 COMMENT 0用户 1管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE goods ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 商品名称, category VARCHAR(30) NOT NULL COMMENT 品类外套/连衣裙/卫衣等, season VARCHAR(10) DEFAULT NULL COMMENT 季节春/夏/秋/冬, price DECIMAL(10,2) NOT NULL COMMENT 售价精确到分, original_price DECIMAL(10,2) DEFAULT NULL COMMENT 吊牌价用于展示折扣, stock INT NOT NULL DEFAULT 0 COMMENT 库存数量, main_image VARCHAR(255) DEFAULT NULL COMMENT 主图路径, images TEXT COMMENT 详情图路径逗号分隔, description TEXT, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE cart ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, goods_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, checked TINYINT DEFAULT 1 COMMENT 是否勾选结算, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_goods (user_id, goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号用时间戳加随机数生成, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, receiver_name VARCHAR(50) NOT NULL, receiver_phone VARCHAR(20) NOT NULL, receiver_address VARCHAR(200) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id INT NOT NULL AUTO_INCREMENT, order_id INT NOT NULL, goods_id INT NOT NULL, goods_name VARCHAR(100) NOT NULL COMMENT 下单时的商品名称快照, goods_image VARCHAR(255) DEFAULT NULL, spec VARCHAR(50) DEFAULT NULL COMMENT 尺码/颜色例如 M 黑色, price DECIMAL(10,2) NOT NULL COMMENT 下单时单价快照, quantity INT NOT NULL, subtotal DECIMAL(10,2) NOT NULL, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;价格用 DECIMAL(10,2) 而不是 float 或 double是因为浮点数在 Java 和 MySQL 里都存在精度丢失问题。一件衣服 99.90 元存成 double 可能变成 99.899999三个商品加出来的总额甚至会出现 0.000001 的偏差答辩时演示取消订单退款金额对不上就尴尬了。DECIMAL 是字符串存储的定点数计算不会丢精度。库存字段和订单状态用整数类型后续做减库存和状态流转时直接数值运算清晰且不容易出错。3.3 为什么要加逻辑删除和上架状态我没有在表结构里加删除时间字段但建议你在实际项目中给 user 和 goods 加一个 deleted 字段通过 MyBatis-Plus 的 TableLogic 注解做逻辑删除。原因很现实毕设项目大概率会有管理员删除商品的功能物理删除会把 order_item 里的商品快照也牵连出问题买家已下单但商品被删了明细查不出来订单页面就会显示异常。逻辑删除只是把 deleted 标记为 1查询时自动过滤掉历史数据还在订单页面永远能查到那个商品的名字和图片。goods 表里的 status 上架/下架字段同样重要。时装商品经常需要临时下架某件衣服比如库存不够了但管理员只想把它从列表里藏起来不是删掉。前端商品列表和详情接口都要带 status1 这个条件。TableLogic 处理的是“删除”status 是“上下架”两者不要混用一个走默认逻辑删除拦截一个走手写 SQL 条件责任清晰。4. 从登录到下单四个核心模块的代码落地4.1 登录注册BCrypt 加密与登录拦截器登录注册几乎是购物系统的门面模块。很多毕设源码图省事用 MD5 存密码答辩时老师问一句“MD5 为什么不够安全”你很难圆回来。正确做法是用 BCrypt 加密Spring Security 里自带 BCryptPasswordEncoder但只引这个工具不需要引入整个安全框架。MyBatis-Plus 也没内置最常见的做法是单独引入 spring-security-crypto 依赖或者直接在 pom 里把 spring-boot-starter-security 加进来但只借它的加密类不启用过滤器链。注册时把明文密码转成 BCrypt 哈希再入库登录时把用户输入的明文和数据库里的哈希做匹配。下面是注册接口的核心逻辑重点是密码处理方式。Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Autowired private BCryptPasswordEncoder passwordEncoder; Override public RString register(String username, String password) { // 1. 判空和长度校验username 3-20 位password 6-20 位 if (username null || username.length() 3 || username.length() 20) { return R.error(用户名长度需在3到20位之间); } if (password null || password.length() 6) { return R.error(密码长度不能少于6位); } // 2. 检查用户名是否已被注册 QueryWrapperUser queryWrapper new QueryWrapper(); queryWrapper.eq(username, username); Long count userMapper.selectCount(queryWrapper); if (count 0) { return R.error(用户名已存在); } // 3. 密码加密后保存 User user new User(); user.setUsername(username); user.setPassword(passwordEncoder.encode(password)); user.setNickname(username); user.setRole(0); userMapper.insert(user); return R.success(注册成功); } }这里有三个细节值得说明。QueryWrapper 是 MyBatis-Plus 的条件构造器eq 方法生成 WHERE username ?selectCount 返回记录数完全不需要手写 XML。passwordEncoder.encode 每次生成的哈希串都不同这是 BCrypt 内置随机盐的效果所以不要拿两个注册用户的密码哈希值做比对来判断密码相同比对用 passwordEncoder.matches(明文, 哈希)。最后注册成功直接返回不用自动登录让用户回到登录页输入一次流程更清晰。登录之后要解决“怎么记住登录状态”的问题。毕设级别用 HttpSession 就够了不需要引入 JWT 那套复杂机制。登录成功把 userId 和 username 放进 session然后写一个拦截器拦截除登录注册和商品浏览以外的路径检查 session 里有没有 userId没有就重定向到登录页。下面这个拦截器实现可以直接抄。Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Integer userId (Integer) session.getAttribute(userId); if (userId null) { // 前端分离项目返回 JSON 状态码页面跳转项目则重定向 response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } return true; } }判断是否放行的核心是 session 里的 userId。拦截器里区分两种前端方案如果 Vue 首饰分离返回 401 状态码让前端跳到登录页如果是 Thymeleaf 服务端渲染直接 response.sendRedirect(/login) 转向登录页。实际写代码时没有第二个 if我在注释里标注了两条路径的区别。写完拦截器后记得在 WebMvcConfigurer 里注册并放行登录注册和静态资源否则会出现“登录页本身进不去”的幻之报错。Configuration public class WebConfig implements WebMvcConfigurer { Autowired private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns( /api/user/login, /api/user/register, /api/goods/**, /, /index.html, /static/**, /css/**, /js/**, /img/** ); } }excludePathPatterns 里必须把静态资源路径全部放行。Vue 打包后的页面入口是 /index.htmljs 和 css 目录也要放开不然页面加载到一半就被拦回登录页控制台一片 401。api/goods/** 是商品浏览接口游客也要能看不能拦截。4.2 商品列表与分页不用手写 LIMIT 的 MyBatis-Plus 分页写法商品列表是时装购物系统最常演示的前台页面一页显示 8 到 12 件商品支持按品类、季节、价格区间过滤。这个模块我用 MyBatis-Plus 的分页插件来做对比手写 LIMIT 的好处是它同时返回当前页数据和总条数前端底部分页条直接绑定这两个值。先要注册一个 MybatisPlusInterceptor 的 Bean这是分页插件生效的前提。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }分页插件的作用是拦截 MyBatis-Plus 内部生成的 SQL自动在末尾拼接 LIMIT所以业务代码里完全不需要写 limit 关键字。下面是商品列表 Service 层的分页查询逻辑。public PageGoods getGoodsPage(int pageNum, int pageSize, String category, String season) { PageGoods page new Page(pageNum, pageSize); LambdaQueryWrapperGoods wrapper new LambdaQueryWrapper(); wrapper.eq(Goods::getStatus, 1); // 只查上架商品 if (category ! null !category.isEmpty()) { wrapper.eq(Goods::getCategory, category); } if (season ! null !season.isEmpty()) { wrapper.eq(Goods::getSeason, season); } wrapper.orderByDesc(Goods::getCreateTime); // 新品在前 return goodsMapper.selectPage(page, wrapper); }LambdaQueryWrapper 的写法能直接在编译期校验字段名是否正确Goods::getStatus 写错了会报编译错误而 QueryWrapper 里用字符串 status 拼错只能运行时才暴露。orderByDesc 是按下架时间倒序这是时装类商品的常规排列新品优先。selectPage 是 MyBatis-Plus 内置方法第一个参数是分页参数第二个是查询条件返回的 Page 对象里有 records、total、current、size 四个属性直接传给前端。分页最容易翻车的地方是前后端页码约定不统一。前端传的 pageNum 从 1 开始后端 Page 对象里的 current 也是从 1 开始这俩天然对齐。但如果有源码为了省事把 current 初始化为 0前端第一页会查出空数据。调试方法很简单打印 Page 对象的 toString看到 SQL 日志里的 LIMIT 0,8 就说明第一页没问题如果看到 LIMIT 0,0 那就是 pageSize 被传成了 0。4.3 购物车临时数据的增删改查购物车是用户维度的临时数据挂在登录用户下。查询接口拿 userId 查 cart 表再关联 goods 表把商品详情查出来。这里不推荐一条 join SQL 把所有数据查完更常见的做法是分两步先查购物车记录列表再用 goodsId 集合批量查商品信息。原因是购物车记录本身要存加入时间、勾选状态和商品表不是一一对应关系用 join 容易查出重复数据。public ListCartItemVO getCartList(Integer userId) { // 第一步查购物车表 LambdaQueryWrapperCart wrapper new LambdaQueryWrapper(); wrapper.eq(Cart::getUserId, userId); wrapper.orderByDesc(Cart::getCreateTime); ListCart cartList cartMapper.selectList(wrapper); // 第二步批量查商品 ListInteger goodsIds cartList.stream().map(Cart::getGoodsId).collect(Collectors.toList()); ListGoods goodsList goodsMapper.selectByIds(goodsIds); // 第三步组装购物车视图对象 MapInteger, Goods goodsMap goodsList.stream() .collect(Collectors.toMap(Goods::getId, goods - goods)); ListCartItemVO result new ArrayList(); for (Cart cart : cartList) { Goods goods goodsMap.get(cart.getGoodsId()); if (goods null) { continue; // 商品被删则跳过 } CartItemVO vo new CartItemVO(); vo.setCartId(cart.getId()); vo.setGoodsId(goods.getId()); vo.setGoodsName(goods.getName()); vo.setPrice(goods.getPrice()); vo.setQuantity(cart.getQuantity()); vo.setImage(goods.getMainImage()); vo.setCheck(cart.getChecked()); result.add(vo); } return result; }这段代码的核心思路是内存组装而非循环查询避免在列表里一条一条查数据库造成 N 次查询。selectByIds 是 MyBatis-Plus 3.4 以后提供的方法传入 ids 集合返回列表。如果用的版本比较老没有这个方法用 selectBatchIds 替代效果一样。第三步里的 goods null 判断是防御性写法购物车里留有已删除商品时跳过它而不是报空指针。购物车的加购接口比列表简单但要注意一个业务细节同一个用户反复点击同一个商品不要新增两条记录应该把已存在的那条 quantity 加一。先查再插或者用 unique 索引加 on duplicate key update 都能实现。我习惯用先查再插因为逻辑直观答辩时好解释。4.4 下单事务库存扣减与订单生成的原子性下单是整个系统里技术含量最高的一个点也是答辩时最容易被追问的部分。常规页面上的下单操作要干三件事查购物车勾选商品、计算总价、生成订单和明细。如果把这三件事拆成三个接口中间任何一步失败前端就得处理半完成状态。正确做法是在后端用 Transactional 把它们包成一个原子操作。Transactional(rollbackFor Exception.class) public OrderVO createOrder(Integer userId, String receiverName, String receiverPhone, String receiverAddress) { // 1. 查购物车中勾选的商品 ListCart checkedCarts cartMapper.selectList( new LambdaQueryWrapperCart() .eq(Cart::getUserId, userId) .eq(Cart::getChecked, 1) ); if (checkedCarts.isEmpty()) { throw new BizException(购物车为空或未勾选商品); } // 2. 扣库存并计算总金额 BigDecimal total BigDecimal.ZERO; ListGoods goodsList new ArrayList(); for (Cart cart : checkedCarts) { Goods goods goodsMapper.selectById(cart.getGoodsId()); if (goods null || goods.getStock() cart.getQuantity()) { throw new BizException(商品库存不足 goods.getName()); } goods.setStock(goods.getStock() - cart.getQuantity()); goodsMapper.updateById(goods); total total.add(goods.getPrice().multiply(BigDecimal.valueOf(cart.getQuantity()))); goodsList.add(goods); } // 3. 生成订单号、插入订单表 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); order.setReceiverName(receiverName); order.setReceiverPhone(receiverPhone); order.setReceiverAddress(receiverAddress); orderMapper.insert(order); // 4. 批量插入订单明细 for (int i 0; i checkedCarts.size(); i) { Cart cart checkedCarts.get(i); Goods goods goodsList.get(i); OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setGoodsId(goods.getId()); item.setGoodsName(goods.getName()); item.setGoodsImage(goods.getMainImage()); item.setSpec(cart.getSpec()); item.setPrice(goods.getPrice()); item.setQuantity(cart.getQuantity()); item.setSubtotal(goods.getPrice().multiply(BigDecimal.valueOf(cart.getQuantity()))); orderItemMapper.insert(item); } // 5. 清空已下单的购物车 cartMapper.delete( new LambdaQueryWrapperCart() .eq(Cart::getUserId, userId) .eq(Cart::getChecked, 1) ); OrderVO vo new OrderVO(); vo.setOrderId(order.getId()); vo.setOrderNo(order.getOrderNo()); vo.setTotalAmount(total); return vo; }Transactional 注解最关键的两个属性是 rollbackFor 和事务边界。rollbackFor Exception.class 表示任何异常都触发回滚如果不写这个参数Spring 默认只在 RuntimeException 时回滚受检异常不会回滚后果是库存扣了但订单没生成数据不一致。事务边界是方法体只要方法没有执行完抛异常所有已执行的数据库操作全部回滚。扣库存用了先查再改的方式selectById 查出当前库存判断足够后 updateById 减掉数量。这是最简单的库存控制方式但存在并发超卖问题。两个用户同时下单并且库存只剩 1 件时两边都读到库存 1都能通过判断结果库存变成 -1。如果你想在毕设里体现一点进阶思路可以把更新库存的 SQL 改成带条件的写法UPDATE goods SET stock stock - 1 WHERE id ? AND stock 1影响行数为 0 就说明库存不足。这个升级点答辩时主动讲出来老师会明显觉得你有并发意识。订单号的生成方式用一个简单方案就够年月日时分秒加用户ID加三位随机数。不要用数据库自增 ID 当订单号展示给用户它会泄露当日订单总量也不方便在客服沟通时快速记忆。generateOrderNo 方法把 LocalDateTime 格式化成 yyyyMMddHHmmss 再拼接控制在 32 位以内保证唯一性。5. 开发时装购物系统必踩的五个坑版本、事务与静态资源排查5.1 springboot 版本太高导致 MyBatis-Plus 不兼容如果你自己新建项目时选了最新版 Spring Boot很可能遇到 MyBatis-Plus 的ClassNotFound或者分页插件不生效的问题。Spring Boot 3.x 使用了 Jakarta EE 规范包名从 javax.* 换成 jakarta.*MyBatis-Plus 3.4 及以下版本按 javax 编译跑在 Boot 3 上必然报错。我给你的建议是直接选 Spring Boot 2.7.x 稳定版对应 MyBatis-Plus 3.5.x全套依赖都能正常拉取网上绝大多数教程代码也都是这个版本体系。如果非要用 Boot 3必须升级到 mybatis-plus-spring-boot3-starter 这个专门的分支依赖普通 starter 不适用。这个选择能让你省出两三天时间放在页面和数据上非常值得。5.2 金额用 double 还是 BigDecimal翻车现场单价 59.90 元、数量 3 件double 计算结果是 179.99999999999997 而不是 179.70。这个现象在 Java 里必现因为 double 是二进制浮点数无法精确表示 0.1 这类十进制小数。购物车勾选了一件单价 99.90 和一件单价 59.90 的商品前端合计显示 159.80后端订单却存了 159.80000000000001对不上。解决方式就是前面建表时用的 DECIMAL(10,2)配合 Java 侧 BigDecimal 做加法和乘法最后通过 setScale(2, RoundingMode.HALF_UP) 四舍五入到分。任何涉及金额的实体属性都用 BigDecimal前端传金额进来也要用 new BigDecimal(String) 而不是 new BigDecimal(double)后者会把 double 的误差带进来。这个习惯一旦养成后面做退款、统计报表时能少掉很多头发。5.3 时区问题为什么 createTime 差了 8 小时数据库里的 create_time 存的是 2024-06-01 12:00:00页面却显示 2024-06-01 04:00:00这就是典型的时区偏移。MySQL 8.x 默认时区是 UTC中国是 UTC8JDBC 连接时如果不指定 serverTimezone驱动会用 JVM 所在时区而 JVM 在东八区MySQL 在 UTC两边一转换就差了 8 小时。前面 application.yml 里写的 serverTimezoneAsia/Shanghai 就是为了解决这个。如果你的项目还是出现时间偏移检查 MySQL 服务端时区配置执行 SELECT NOW() 看返回的当前时间是否和本地一致。也可以在连接 URL 里同时加 useSSLfalse 和 allowPublicKeyRetrievaltrueMySQL 8.x 默认不开放公钥检索某些连接场景会因此报错。5.4 静态资源被拦截器拦截登录页进不去最诡异的现象是启动项目后访问首页直接 401但后端代码看起来没有错。排查步骤是这样先看控制台有没有拦截器日志确认请求是否进入了 preHandle再看拦截器注册代码里的 excludePathPatterns 是否覆盖了当前路径。注意 Spring Boot 的默认静态资源路径是 classpath:/static/访问根路径 / 时若没有 index.html 映射会直接走 DispatcherServlet绕过静态资源处理进入拦截器。Vue 打包后 index.html 在 static 根目录下访问 / 应该能直接打开不需要任何 controller。如果你手动写了 RequestMapping(/) 返回了某个视图反而会盖掉默认的静态首页。我的习惯是根路径放行排查顺序从静态资源路径有没有拼错开始逐条核对排除列表。防止登录页被拦截的另一个土办法是先不配拦截器把功能全部开发完再做登录校验被这个坑拖住排查一晚上不划算。5.5 跨域问题为什么页面能打开但接口全报错前后端分离方案最大的坑就是跨域。Vue 开发时启动在 5173 端口Spring Boot 在 8080 端口前端请求 /api/goods/list 时浏览器会先发 OPTIONS 预检请求后端若不处理跨域预检直接失败业务请求根本发不出去。解决方式是在 Spring Boot 里配置全局跨域支持。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); } }addMapping(/**) 对所有接口生效allowedOriginPatterns() 允许所有来源allowCredentials(true) 允许携带 Cookie。但注意一个细节allowedOriginPatterns() 和 allowCredentials(true) 同时使用时不能用 allowedOrigins(*)Spring 会抛 IllegalArgumentException因为它不允许带凭据的请求使用通配符来源。如果部署上线后接口报 403 跨域错误优先检查浏览器控制台里的具体提示看是“不允许的 Origin”还是“不允许的请求头”再针对性地改这个配置。6. 让购物系统从能跑走向能答辩演示数据、接口文档与部署技巧系统能跑通和能拿高分中间还隔着三个工程我用最后这章把它们的距离拉近。第一个工程是演示数据。很多项目跑起来后页面上只有三五件测试商品还挂着默认路径的破图答辩时投屏一放大观感直接降级。时装购物系统的演示数据最少要有 12 到 16 件商品覆盖四个品类、四个季节、不同价格带。每件商品配一张高清主图和至少两张详情图图片路径不要带本地磁盘绝对地址要存相对路径或完整 URL否则部署到别的机器就碎了。图片批量导入的常见做法是写一个 DataInitializer应用启动时检测商品表为空就自动从 resources 下导入预定数据这样每题一次演示都不用重新人工录入。第二个工程是接口文档。Spring Boot 项目集成 Swagger 只需要加一个 springfox-boot-starter 依赖在 controller 类上用 Api 注解描述模块在接口方法上用 ApiOperation 写一行说明访问 /swagger-ui/index.html 就能看到所有接口的请求参数和返回结构。答辩时现场打开 Swagger 页面比翻代码讲接口规范直观得多老师点哪个接口你就演示哪个能省下大量组织语言的时间。Swagger 生成页面里能直接点按钮发起请求相当于自带了一个简易调试工具也方便你提前把所有接口都点一遍做自测。第三个工程是部署演示。毕设答辩通常在机房或教室进行网络环境不稳定不要指望现场连外网。最稳妥的方案是本地打成 jar 包用 java -jar 启动浏览器访问 localhost:8080。打包前注意 application.yml 里数据库地址是 localhost且 MySQL 服务已启动把演示依赖的数据库脚本一起放在项目根目录的 sql 文件夹里。打成可执行 jar 后记得先在有数据的机器上跑一遍完整购物流程再提交不然现场发现端口被占或者数据库没启动心态容易崩。部署前先关闭本地多个占用 8080 的进程用以下命令确认。# 查看 8080 端口占用情况 netstat -ano | findstr :8080 # 找到 PID 后强制结束进程 taskkill /PID 8080 /F # 启动打包后的 jar java -jar fashion-shop-0.0.1-SNAPSHOT.jar --server.port8080--server.port 是 Spring Boot 的命令行参数可以临时覆盖 application.yml 里的端口配置现场如果 8080 被其他程序占用改成 8081 即可不用改配置重启进程。这个参数在答辩现场比改文件再重新打包快得多是非常值得记的一个技巧。线下把 jar 包和数据库脚本一起复制到 U 盘或者本机目录里配合 Swagger 和一些说明文档就能应对大多数演示环境。我在做过几个类似毕设辅导后最大的教训是把项目从“代码能编译”推进到“一整套故事能讲通”才是这类系统最花时间的环节。数据要好看、接口要有文档、部署要能一键然后在演示时主动讲出第三节下单事务里那个并发库存更新的思考这比多堆十个 CRUD 功能都有说服力。只要你在建表时坚持了 tid10 的那份克制、字段类型按这条路子走你和这套系统的故事基本稳了。希望这篇文章能让你少走我当年的那几段弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑