ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot建材商城毕设实战:从数据库设计到下单全流程

SpringBoot建材商城毕设实战:从数据库设计到下单全流程 这阵子被问得最多的一个毕设选题就是 Java SpringBoot 做装修建材商城。这类题目在计算机毕业设计里出现频率极高原因不复杂建材商品天然具备多规格、多分类、价格区间大这些电商典型特征一套商城系统做下来CRUD、权限、购物车、订单、库存这些核心能力全覆盖答辩有东西可讲代码量又控制在合理范围内。我帮人复审过不少类似项目自己也带过两届课设。这套 SpringBoot 建材电商平台的完整落地过程我整理成一篇实操向的记录从技术选型、数据库设计到登录鉴权、商品模块、下单流程再到开发中的翻车现场和答辩加分点一次说清楚。无论你是准备拿它当毕业设计还是想快速上手一个能跑的电商项目这篇文章都可以直接当施工图用。1. 项目整体设计与技术选型思路1.1 为什么建材商城这个选题适合做毕设先说定位。建材装修和普通卖衣服的商城最大的区别是商品模型更复杂瓷砖要按尺寸、色号、哑光亮光区分板材要按厚度、环保等级区分卫浴还可能涉及安装服务。这意味着你不能只建一张简单的商品表必须认真设计多规格、多图片、品牌筛选、分类树这些结构。换句话说这个选题天然逼着你把电商系统的核心难点都过一遍。从毕业设计评分角度看这个题目的优势更明显需求明确网上能搜到大把同类系统做参考不会出现题目太大做不完的问题。功能边界清晰前台商城 后台管理是标准结构拆分合理。技术栈主流Java SpringBoot MySQL 是招聘市场上最常见的组合写进简历有说服力。很多同学纠结要不要上微服务、要不要搞分布式我的建议很直接毕设的核心是完整跑通、逻辑自洽、能讲清楚而不是堆技术名词。单体应用做到结构干净、事务正确、异常处理到位分数不会低。1.2 前后端分离还是服务端渲染怎么选这是动工之前必须做的第一个决定。我见过太多同学做了一半发现页面写不动回头换方案时间全浪费了。两种方案对比方案技术组合优点风险点前后端分离SpringBoot Vue 3 Element Plus前后端职责清晰接口文档一目了然答辩好讲需要额外掌握 Node、Vite工作量略大服务端渲染SpringBoot Thymeleaf工程简单一个项目跑到底环境依赖少页面和后端耦合动态交互体验一般我推荐前后端分离因为答辩老师最常问的问题就是你这个接口怎么设计的、数据怎么传的。分离方案下Vue 页面、Controller 接口、数据库表三层的对应关系非常清楚直接对着接口文档讲就行。时间特别紧的同学再去考虑 Thymeleaf 保底。那套建材商城的实际结构是后端 SpringBoot 2.7 MyBatis-Plus MySQL 8 Redis前端 Vue 3 Vite Element Plus Pinia一套后端接口同时支撑商城主页和后台管理页代码里天然体现了多端共用接口的设计思路。1.3 功能清单拆解前台和后台各做什么开工前一定要把功能清单写死不然后期容易失控。这套系统我拆成两大块前台商城模块用户注册登录手机号 密码JWT 令牌鉴权首页Banner 轮播、热门建材推荐、分类入口商品浏览一级分类 二级分类树关键词搜索品牌和价格区间筛选商品详情多规格切换、图文详情、实时库存显示购物车加入、删除、修改数量、批量勾选结算下单结算收货地址选择、订单确认、提交生成订单订单中心待付款、待发货、待收货、已完成查看物流信息模拟个人中心地址管理、个人信息修改后台管理模块管理员登录与权限拦截商品管理新增编辑、上下架、库存调整分类管理一级二级分类维护订单管理订单查询、发货操作、订单备注用户管理用户列表、禁用启用轮播图管理简易统计订单总量、销售额、热销商品排行没有引入真实支付和物流对接这是刻意为之。毕设阶段用支付宝沙箱或者干脆做成模拟支付状态流转即可重点是把交易状态机讲清楚而不是真的去对接第三方。2. 数据库设计建材商品的数据模型怎么建才合理2.1 核心表结构盘点数据库设计是这类项目的灵魂答辩时问得最多的也是这里。整套系统我拆了 9 张核心表不多不少用户表、分类表、商品表、商品规格表、购物车表、订单表、订单明细表、收货地址表、轮播图表。用户表设计密码绝不能明文存储我用 BCrypt 加密字段长度给到 100CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名/登录账号, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, phone varchar(20) DEFAULT NULL COMMENT 手机号, nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(255) DEFAULT NULL COMMENT 头像URL, status tinyint DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;商品表是重点我加了 sales 字段做热销排序加 status 控制上下架CREATE TABLE product ( id bigint NOT NULL AUTO_INCREMENT, category_id bigint NOT NULL COMMENT 所属分类, name varchar(100) NOT NULL COMMENT 商品名称, subtitle varchar(255) DEFAULT NULL COMMENT 副标题, main_image varchar(255) DEFAULT NULL COMMENT 主图URL, detail_html text COMMENT 图文详情HTML, price decimal(10,2) NOT NULL COMMENT 展示价, stock int DEFAULT 0 COMMENT 总库存, brand varchar(50) DEFAULT NULL COMMENT 品牌, status tinyint DEFAULT 1 COMMENT 1上架 0下架, sales int DEFAULT 0 COMMENT 销量, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.2 商品多规格和分类树建材商品的特殊之处普通商品表一个 SKU 就完事建材不行。比如一款 800×800 的瓷砖有不同色号、不同表面工艺价格还不一样。所以我单独拆了一张product_sku表CREATE TABLE product_sku ( id bigint NOT NULL AUTO_INCREMENT, product_id bigint NOT NULL, spec_desc varchar(255) DEFAULT NULL COMMENT 规格描述如 800x800/浅灰色/亮光, price decimal(10,2) NOT NULL, stock int NOT NULL DEFAULT 0, image varchar(255) DEFAULT NULL, PRIMARY KEY (id), KEY idx_product (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;商品详情页选择规格时前端把选中的规格组合传给后端后端根据spec_desc匹配出对应的 SKU 价格和库存。这样设计的好处是以后上新品带宽度的地板、多色号的乳胶漆都能直接套用。分类树我采用经典的 parent_id 方案一级分类是瓷砖地板卫浴涂料门窗这些大类二级分类是抛光砖仿古砖这种细分。MyBatis-Plus 里查子分类就是category_id ? AND parent_id ?简单直观答辩也好解释。2.3 订单表和状态机设计什么时候会翻车订单表是这套系统里最容易出问题的表设计时我特别加了两个关键点一是订单号唯一索引。不要用自增主键直接当订单号订单号要可读、可追踪我生成规则是前缀 时间戳 随机数比如BM20240612103015001并且建唯一索引防止并发重复。二是状态字段用整数而不是字符串CREATE TABLE orders ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id bigint NOT NULL, total_amount decimal(10,2) NOT NULL COMMENT 商品总额, pay_amount decimal(10,2) NOT NULL COMMENT 实付金额, status tinyint DEFAULT 0 COMMENT 0待付款 1待发货 2待收货 3已完成 4已取消, receiver_name varchar(50) DEFAULT NULL, receiver_phone varchar(20) DEFAULT NULL, receiver_address varchar(255) DEFAULT NULL, pay_time datetime DEFAULT NULL, delivery_time datetime DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单明细表order_item记录下单那一刻的商品快照 JSON 或者冗余字段包括商品名、规格、单价、数量。这里必须冗余商品名称和价格快照不能只存商品 ID。道理很简单商品后来改价了、改名了历史订单里的信息必须还是下单时的样子。这个细节在答辩时主动讲出来老师会认为你真的理解业务。金额字段一律decimal(10,2)谁用 float 存价格谁后悔0.1 0.2 的浮点误差问题在电商场景里是致命的。3. 核心功能实现从登录到下单的完整链路3.1 工程初始化依赖版本怎么锁SpringBoot 版本选择上我踩过一次大坑。一开始图新鲜用了 SpringBoot 3.x结果发现 JDK 版本要求 17 起步很多同学电脑还是 JDK 8而且 3.x 里javax.servlet全家改成了jakarta.servlet按老教程写代码直接编译不过。所以毕设项目我强烈建议锁 SpringBoot 2.7.18这是 2.x 系列最后一个版本稳定、教程多、兼容 JDK 8 和 JDK 11。pom.xml 核心依赖parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies注意 MyBatis-Plus 和 SpringBoot 版本有对应关系3.5.5 配 2.7.x 没问题别随便升到 3.5.9 配 3.x 的坑。Redis 在毕设里不是为了炫技它后面用来做令牌黑名单和首页缓存量不大但够讲。application.yml 里最容易配错的是 MySQL 驱动和时区server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/build_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted老版本的com.mysql.jdbc.Driver已经被移除了必须用com.mysql.cj.jdbc.Driver没有serverTimezoneAsia/Shanghai的话时间字段会差 8 个小时这两个是最常见的启动报错来源。3.2 JWT 登录鉴权不做 Session 的原因后端接口不能裸奔除了注册登录接口其余接口都要校验身份。我选用 JWT 而不是 Session讲两个理由第一前后端分离架构下后端无状态前端拿到 token 存 localStorage每次请求带在 Header 里后端不用维护会话第二答辩时可以讲清楚 token 的组成结构——Header、Payload、Signature 三段。生成令牌的工具类public class JwtUtil { private static final String SECRET build-mall-secret-key-change-in-prod; private static final long EXPIRE 7 * 24 * 60 * 60 * 1000L; // 7天毫秒数 public static String createToken(Long userId, String username) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Long getUserId(String token) { Claims claims Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); return Long.valueOf(claims.getSubject()); } }然后注册一个拦截器把需要登录的接口拦下来从 Header 里取Authorization解析出用户 ID 放进 request 属性public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { try { Long userId JwtUtil.getUserId(token.substring(7)); request.setAttribute(userId, userId); return true; } catch (Exception e) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } } response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } }登录时的密码校验用 Spring Security 里的 BCryptPasswordEncoder也可以只引入 spring-security-crypto 这一个轻量依赖不必把整套 Spring Security 引进来。注册时encoder.encode(rawPassword)登录时encoder.matches(rawPassword, encodedPassword)。明文密码入库这个错误很多同学觉得无所谓一旦答辩被问到密码安全就哑火了。3.3 商品列表与搜索接口设计的一种标准写法商品列表接口我统一走一个分页查询接口参数包含分类 ID、关键词、品牌、最低价、最高价、排序方式、页码、每页条数。Controller 层用 MyBatis-Plus 的 LambdaQueryWrapper 动态拼接条件Override public IPageProductVO pageProducts(ProductQuery query) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); if (query.getCategoryId() ! null) { wrapper.eq(Product::getCategoryId, query.getCategoryId()); } if (StringUtils.hasText(query.getKeyword())) { wrapper.like(Product::getName, query.getKeyword()); } if (StringUtils.hasText(query.getBrand())) { wrapper.eq(Product::getBrand, query.getBrand()); } if (query.getMinPrice() ! null) { wrapper.ge(Product::getPrice, query.getMinPrice()); } if (query.getMaxPrice() ! null) { wrapper.le(Product::getPrice, query.getMaxPrice()); } wrapper.eq(Product::getStatus, 1); // 只查上架商品 if (sales.equals(query.getSort())) { wrapper.orderByDesc(Product::getSales); } else { wrapper.orderByDesc(Product::getCreateTime); } PageProduct page new Page(query.getPageNum(), query.getPageSize()); return productMapper.selectPage(page, wrapper); }这里有一个容易被忽略的业务点查询条件里必须强制过滤status 1只展示上架商品。后台编辑商品存草稿、下架商品前台就不该搜得到。这个细节看起来小但能体现你有没有考虑前后台数据隔离。3.4 下单流程事务、库存扣减和超卖问题下单是整个系统最关键的一段代码很多毕设在这里翻车原因是没有处理好并发扣库存。先看核心逻辑Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, CreateOrderRequest req) { // 1. 查询用户勾选的购物车商品 ListCartItem items cartMapper.findCheckedItems(userId); if (items.isEmpty()) { throw new BizException(请先勾选要结算的商品); } // 2. 生成订单号 String orderNo BM System.currentTimeMillis() String.format(%03d, new Random().nextInt(1000)); // 3. 创建订单主记录先算金额 BigDecimal total items.stream() .map(i - i.getPrice().multiply(BigDecimal.valueOf(i.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); Order order buildOrder(orderNo, userId, total, req); orderMapper.insert(order); // 4. 逐条扣库存同时写入订单明细 for (CartItem item : items) { int rows productMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows 0) { throw new BizException(商品【 item.getProductName() 】库存不足); } OrderItem oi buildOrderItem(order.getId(), item); orderItemMapper.insert(oi); } // 5. 删除购物车中已结算商品 cartMapper.deleteCheckedItems(userId); return order.getId(); }扣库存的 SQL 是重点用条件更新防止超卖UPDATE product SET stock stock - #{num}, sales sales #{num} WHERE id #{productId} AND stock #{num}UPDATE ... WHERE stock num这种写法依赖数据库行锁并发情况下只有一个请求能更新成功返回值是 0 就说明库存不够直接抛异常回滚事务。这是最朴素的乐观锁思路面试官问起如何防止超卖也能答得上来。整个方法加Transactional任何一步失败数据库都会回滚不会出现订单建了但库存没扣、或者库存扣了但订单没建的情况。事务是对这个问题的唯一正确解不要靠 try-catch 假装处理。3.5 后台管理端商品上架与订单发货后台管理端我单独建了一套/admin前缀的接口拦截器里白名单只放行管理员账号。商品管理的核心是表单提交 图片上传图片上传有一个常见的坑后端保存文件到本地磁盘目录后访问不到。解决方法是配置静态资源映射把/upload/**指到真实磁盘路径Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadDir D:/build-mall-upload/; registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir); } }数据库里只存/upload/2024/06/xxx.jpg这种相对路径前端拼上域名访问。千万别在数据库里存localhost:8080开头的绝对地址不然部署到服务器就全裂了。4. 开发中常见问题与排查技巧实录4.1 SpringBoot 版本太高引发的连锁反应实话说我见过太多同学栽在版本选择上。SpringBoot 3.x 出来之后网上大量新教程直接跳到 3.x但配套的 MyBatis-Plus 版本、JDK 版本、第三方组件兼容性全部要跟着变一步没对齐就报错。排查思路就一条先看报错的第一行 Caused by然后去 Maven 仓库确认版本对应关系。SpringBoot 2.7.18 搭配 JDK 8 是毕设场景最稳的组合所有网上教程都适用。没有特殊需求就别追新追新的成本全得自己买单。4.2 跨域问题前端连不上的头号原因Vue 前端跑在 5173 端口后端跑在 8080浏览器直接请求必然有跨域问题。两个解决办法第一个是后端放开跨域Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); }第二个更推荐前端 Vite 配置代理把/api前缀的请求转到后端// vite.config.js server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } }两种方式我都用过。实际开发建议用 Vite 代理因为路由器里不需要额外配置生产部署时前端打包后放进 SpringBoot 的static目录同源访问根本不存在跨域问题——这也是热词里vue 打包放进 springboot那个操作的实际场景。4.3 接口返回时间格式和时区问题调试时发现订单创建时间比本地时间晚了 8 个小时十有八九是 JDBC 连接串没加serverTimezoneAsia/Shanghai或者数据库连接时区是 UTC。这个坑非常隐蔽因为它不影响功能只影响显示很多同学查半天查不出来。统一做法是数据库连接串加时区、Jackson 全局配置日期格式、Java 实体时间字段用LocalDateTime而不是Date。三层对齐时间就永远不会乱。4.4 并发下单导致的库存变负数本地演示时一个人操作看不出问题但答辩现场老师可能会问如果两个人同时买最后一件商品会怎样。如果你只用SELECT stock再在 Java 里判断、然后UPDATE stock stock - 1并发时两个请求都读到库存 1然后都更新成 0就会出现超卖。解法就是前面写的条件更新 SQLUPDATE product SET stock stock - #{num} WHERE id #{id} AND stock #{num}更新的行数为 0 就抛异常。把这个原理画成流程图讲给答辩老师直接是加分项。4.5 MyBatis-Plus 的字段映射和逻辑删除实体里createTime对应表里create_timeMyBatis-Plus 默认开启驼峰映射一般不用管。但detailHtml对应detail_html这种多词字段如果没生效检查配置里是否开了map-underscore-to-camel-case: true。逻辑删除我也开了在实体加TableLogic注解的deleted字段删除操作自动转成 UPDATE防止用户数据被物理删除。答辩被问到数据安全性时这是一个相当实用的回答点。5. 项目演示与答辩准备的加分项5.1 演示环境保证 Demo 不翻车的三个细节毕设答辩最怕的就是现场演示崩溃。我的经验是提前做好三件事第一准备一份初始化 SQL把商品数据、分类数据、管理员账号都预置好答辩前重新执行一遍保证数据干净。第二把前端打包产物放到 SpringBoot 的static目录下一个 jar 包启动后浏览器直接访问 8080 端口就能看到完整系统不用现场再启一个前端服务。这也是热词里一直在搜的vue 打包放进 springboot的标准做法dist文件夹内容拷到src/main/resources/static即可。第三Redis 如果没装或者起不来会拦截登录答辩前先把 Redis 服务启好。很多同学现场翻车就是因为 Redis 没启动登录接口疯狂报错。5.2 答辩讲什么从代码到业务的三个亮点我建议你从三个角度组织答辩陈述业务闭环、技术深度、工程规范。业务闭环讲从浏览商品到提交订单到后台发货这条完整链路是如何走通的强调订单状态机的流转以及商品快照为什么必须冗余到订单明细里。技术深度讲 JWT 鉴权的原理、库存扣减的并发控制、事务回滚机制。这几个点每个都能展开三分钟足够撑起提问环节。工程规范讲项目结构分层controller、service、mapper、entity、dto、common 各司其职统一异常处理和统一返回体。老师翻你代码时看到Result类包装返回、全局异常处理器印象分会明显提升。5.3 还能扩展什么方向如果学有余力这几个扩展方向性价比最高支付接入支付宝沙箱打通真实支付回调流程顺便讲清楚回调幂等性订单超时用 Redis 过期键或定时任务实现超过 30 分钟未付款自动取消订单搜索把商品搜索从 MySQL LIKE 换成 Elasticsearch直接呼应现在热词里springboot 整合 flinkai 搜索平台这些新趋势背后的数据索引思路权限给后台管理引入角色概念做成多管理员权限模型我个人在实际操作中的体会是这类项目最难的不是写代码而是把每一项技术选择背后的理由想清楚。哪怕你只是做一个很常规的 CRUD能讲出为什么用 JWT 不用 Session为什么扣库存要加 stock num 条件为什么订单表要冗余商品快照就已经超过大多数只背代码的同学了。最后再分享一个小经验项目完工后花半天时间把 README 写好把启动步骤、账号密码、技术栈、功能清单全部列清楚。这份文档既是答辩材料也是以后写简历项目描述的素材。毕竟评委看到的不仅是系统能不能跑更是你对自己代码的理解程度。
返回列表