1. 从毕业设计题目出发:这个土特产电商平台到底要做什么
最近不少同学私信问我同一个题目:"基于Spring Boot的雄宗土特产销售网站该怎么下手"。说实话,这个课题在计算机毕业设计里属于非常典型的电商平台类项目,但它又比单纯的学生管理系统、图书管理系统多了一层业务复杂度——它涉及商品展示、购物车、订单流转、支付对接、后台管理等一系列真实电商场景。而"土特产"这个垂直领域又带来了一些特殊需求,比如商品规格差异(散装称重和礼盒包装价格不同)、区域限售、库存单位换算等。
我在帮某高校几位同学复盘这个课题时发现,大多数人第一次看到题目就卡在"从哪开始写代码"上。其实正确的顺序是:先把业务拆清楚,再做技术选型,最后才是写代码。这个题的底层诉求并非让你发明一个新电商模式,而是考察你有没有能力用Spring Boot这个主流框架,把一个完整的、可运行的、有业务深度的交易系统从零搭建出来。
具体到"雄宗土特产"这五个字,它隐含了两个设计前提:一是商品品类偏向农副产品、地方特色食品,这些商品天然适合用图片展示 + 详情描述 + 规格选择的方式来呈现;二是销售模式往往是"产地直发 + 区域性优惠",所以后台需要支持运费模板、库存预警这类电商基础功能。
从技术角度看,这个项目最适合采用前后端分离的结构,后端以Spring Boot为核心,前端可以用Vue或Thymeleaf模板引擎实现。Spring Boot不只是一个框架,它带来的是整个生态:Spring MVC处理请求路由,Spring Data JPA或MyBatis操作数据库,Spring Security做登录鉴权,Redis处理缓存和购物车会话。下面我会按照一个实际可运行的毕设项目标准,把整个设计与实现过程完整过一遍。
2. 技术选型与开发环境准备:为什么是Spring Boot + 这些周边组件
2.1 核心框架选择的底层逻辑
很多教程一上来就建议"选Spring Boot + MyBatis-Plus + MySQL”,但很少有人解释为什么这样组合。我根据实际开发经验帮大家理一下这个选型链条:
Spring Boot解决的是"配置地狱"问题。传统的SSH或SSM项目,光XML配置就能写几百行。Spring Boot通过自动配置和起步依赖(Starter)机制,让你用几行配置就能跑起一个Web应用。对于毕设答辩,你还能专门讲出"自动配置原理""约定优于配置"这些加分点。
MyBatis-Plus则在Spring Boot的JDBC基础上又进一步——它把单表CRUD操作封装到了极致,不需要手写SQL就能完成大部分数据访问。这一点非常重要,因为毕设时间有限,你不能把精力全耗在基础CRUD上,必须留出时间做订单流程、支付逻辑这些真正的业务亮点。MyBatis-Plus的LambdaQueryWrapper写条件查询非常方便,比如按价格区间筛选商品、按销量排序,一行代码就能搞定。
数据库选MySQL不用多说,关键是要注意字符集必须设置为utf8mb4,否则土特产商品名称里的生僻字(比如某些地方特产专用字)会出现乱码。存储引擎建议InnoDB,支持事务——订单创建时的库存扣减和订单记录写入必须在一个事务里,这是电商项目的基本素养。
2.2 开发环境与版本搭配的踩坑提醒
我这边结合多个学生项目的实测情况,给出一套最少踩坑的版本搭配:
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| JDK | 1.8 或 11 | Spring Boot 2.x系列用JDK 8最稳;如果一定要用Spring Boot 3.x,JDK 17起 |
| Spring Boot | 2.7.x | 成熟稳定,网上资料最多,遇到问题容易搜到答案 |
| MySQL | 5.7 或 8.0 | 两者都可,8.0注意驱动要用com.mysql.cj.jdbc.Driver |
| MyBatis-Plus | 3.5.x | 注意与Spring Boot 2.x兼容,别盲目升3.6+ |
| Redis | 6.x | 用于购物车缓存和验证码存储 |
| Maven | 3.8+ | 管理依赖,国内镜像建议配阿里云 |
这里面有一个非常容易踩的坑:Spring Boot 2.7.x默认的Spring Security版本是5.7.x,它的WebSecurityConfigurerAdapter已经标记为过时。如果你照着网上2020年之前的教程写extends WebSecurityConfigurerAdapter,代码能跑但会有一堆"deprecated"警告。更麻烦的是新版写法改成SecurityFilterChain的Bean定义方式,很多同学不适应。我的建议是:既然做毕设,就按新写法来,答辩时还能展示你关注技术演进。
2.3 项目初始化与目录结构设计
创建项目建议直接用Spring Initializr(如果网络不方便,也可以用阿里云的镜像加速地址)。关键是要选对依赖:Spring Web、Spring Data JPA或MyBatis、MySQL Driver、Lombok、Validation。
项目包结构我推荐按业务模块分包,而不是传统的controller/service/dao三层一刀切:
com.xiongzong ├── config(配置类:跨域、MyBatis-Plus分页、Redis序列化) ├── controller(接口层:AdminController、ProductController等) ├── service(业务层:接口+实现) ├── mapper(数据访问层) ├── entity(实体类) ├── dto(数据传输对象:接收前端请求参数) ├── vo(视图对象:返回给前端的数据) ├── common(公共类:统一返回结果、异常处理、常量) ├── utils(工具类:JWT工具、文件上传工具)这种分包方式的好处是:每个功能模块(比如订单)的完整链路——从Controller到Mapper——都聚在一起,答辩时讲解代码结构会非常清晰,而不是在百来人的经典三层结构里来回跳转找代码。
3. 数据库设计:一张表一张表说清楚为什么这么建
3.1 需求分析驱动的核心表划分
我见过不少同学的毕设一号实体建得巨复杂,二十几个字段恨不得把商品的所有属性都塞进去。这其实是设计失误。对土特产电商平台来说,核心表应该围绕**"人、货、单"**三条线展开:
用户线:用户表(存储账号密码、昵称、手机号)、地址表(收货地址,关联用户)。货品线:商品表(基础信息:名称、主图、详情、分类)、商品规格表(价格、库存、规格名)、商品分类表。交易线:购物车表(临时选品)、订单表(订单主信息)、订单明细表(每个商品项的快照)。
此外还有两张辅助表:轮播图表(管理首页广告位)、运费模板表(不同地区不同运费)。
3.2 关键字段的设计细节与理由
**商品表(product)**中最容易忽略的是content字段,它存的是商品详情页的富文本内容。字段类型必须用text或longtext,如果你用varchar(255),前端提交的图文介绍会被截断,而且MySQL在严格模式下会直接报错。另外要预留sales_count字段——"销量"这个值不能每次通过统计订单明细来算,那样数据量一大性能就崩了,正确做法是每次支付成功后在事务里给sales_count + 1。
**商品规格表(product_sku)**是整个数据库设计的亮点。土特产很典型的一种情况是:同一款茶叶,250g装和500g装是两个价格,而且库存独立。把价格和库存从商品表抽出来,设计一张SKU表,产品表只保留最小单价作为展示价。前端规格选择时,通过SKU的ID关联对应价格与库存。
**订单表(orders)**的status字段必须用tinyint而不是varchar字符串。用0、1、2、3表示不同状态:0待付款、1待发货、2待收货、3已完成、4已取消。这个设计配合Java端的枚举类,代码可读性和扩展性都高,也方便数据库索引优化。
三个订单状态的流转有几种特殊路径需要提前想清楚:
- 用户下单后超时未付款,订单要自动取消。这个功能可以用定时任务实现,也可以在有库存回补需求的场景用Redis延迟队列。毕设阶段用Spring的
@Scheduled定时扫描超时订单就够了,但答辩时如果能说出"实际项目中一般会考虑延迟消息或定时补偿"这种话,会很加分。 - 用户在"待发货"状态下申请退款,后台审核通过后订单直接变"已取消",同时库存要自动加回。
- 发货后的退款只能走售后退款通道,这里不做太复杂,后台手动处理就行。
**订单明细表(order_item)**里注意要冗余一份商品名称、商品主图、规格名称和单价快照。为什么?因为商品信息后续会被管理员修改,如果只存商品ID,用户查历史订单时看到的可能是改过的价格和名称,这在真实电商中是不能接受的。下单时商品信息是什么样,订单里就必须永远展现什么样。
3.3 索引设计:毕设里也要有点性能意识
每张核心表的主键一旦构建好,有几个高频查询是你天天都在调的,索引必须提前建好:
orders表的user_id——个人中心查询"我的订单",大概率按时间倒序。order_item表的order_id——订单详情要一次性查出所有明细。product_sku表的product_id——商品详情页需要加载规格列表。cart表的user_id——进购物车页面要查全量。
唯一索引方面,表与表之间若有全局唯一性质的数据,比如users表的username字段,建议建unique index。MyBatis-Plus的saveOrUpdate方法依赖这种约束来实现"有则更新、无则插入"。
4. 后端核心业务模块实现:登录鉴权、商品展示与购物车
4.1 登录注册的完整链路设计
用户登录这块,我强烈推荐用**JWT(JSON Web Token)**而不是传统的Session。原因是:前后端分离后,前端可能部署在另一台服务器,Session在各服务端之间同步很麻烦;而JWT是无状态的,后端只需要在拦截器里校验Token即可。毕设答辩还能顺势讲一句"这更贴近微服务场景下的认证方式",直接拉高评分。
实现步骤上是这样的:
注册接口的逻辑流程是:前端提交用户名、密码、手机号,后端先检查用户名是否已存在,然后密码用MD5加盐哈希(注意不能明文存储),再插入用户表。注册成功后自动返回一个Token,实现"注册即登录"。
登录接口接收用户名和密码,校验通过后生成Token。Token里只放userId和userType(区分普通用户和管理员),过期时间设为24小时。然后设置响应头Authorization: Bearer <token>。
拦截器在Spring MVC里拦截所有/api/**路径,检查Token是否存在且有效。用户端和管理端的鉴权可以共用一套拦截器,通过userType区分权限即可。
这块有一个容易被扣分的点:密码不能明文传参和存储。我之前帮某位同学检查代码,发现用户表里密码字段直接存了明文,这是答辩时的安全隐患,也很容易被评委抓到。正确做法是前端把密码MD5再传参;后端再用Salt + 摘要算法(比如SHA-256)处理。还有一点:用户请求中携带的Token过期后,接口要返回统一的"401未授权"JSON而非白屏或乱码,评审会看你的异常处理是否规范。
4.2 商品列表与详情的分页查询实现
商品模块最核心的两个接口是列表和详情。列表页需要支持分页、分类筛选、按价格排序、关键词搜索。MyBatis-Plus的Page对象配合LambdaQueryWrapper写起来非常简洁:
@Override public Page<ProductVO> getProductPage(ProductQuery query) { Page<Product> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); // 关键词搜索: 匹配商品名称 if (StringUtils.hasText(query.getKeyword())) { wrapper.like(Product::getName, query.getKeyword()); } // 分类筛选 if (query.getCategoryId() != null) { wrapper.eq(Product::getCategoryId, query.getCategoryId()); } // 上架状态,避免把下架商品展示给用户 wrapper.eq(Product::getStatus, 1); // 排序: 默认按创建时间倒序,可选按价格/销量排序 if ("price_asc".equals(query.getSort())) { wrapper.orderByAsc(Product::getMinPrice); } else if ("sales_desc".equals(query.getSort())) { wrapper.orderByDesc(Product::getSalesCount); } else { wrapper.orderByDesc(Product::getCreateTime); } Page<Product> result = this.page(page, wrapper); return convertToVO(result); }注意一个经常被忽略的性能细节:商品列表不要查content富文本字段。你可以用MyBatis-Plus的select()指定只查id, name, main_image, min_price, sales_count这些列表页需要的字段,富文本留在详情接口再查。这样列表页响应快很多。等数据量上了几十万,list查询全量字段很容易变成慢查询。
商品详情接口的返回结构应该嵌套SKU列表:
{ "id": 1, "name": "野生山茶油", "mainImage": "http://...", "content": "<p>产地直发...</p>", "skuList": [ {"skuId": 10, "specName": "500ml单瓶装", "price": 68.00, "stock": 520}, {"skuId": 11, "specName": "1L两瓶礼盒装", "price": 128.00, "stock": 300} ] }前端拿到这个结构就能渲染多规格选择器。价格展示规则可以统一一下:如果SKU价格有高有低,显示"68~128元",如果只有一个规格,就直接显示单个价格。
4.3 购物车:缓存优先还是数据库优先?
购物车是土特产电商平台里一个有趣的设计点。技术上有两条路:
方案一是存数据库,建一张cart表,user_id + product_id + sku_id确定唯一记录,改数量直接update。方案二是存Redis,用Hash结构,key为cart:{userId},field为skuId,value为选中数量。
我推荐毕设阶段选方案一,理由有三点:第一,数据持久化,用户换设备登录购物车还在,体验完整;第二,实现复杂度低,不用操心Redis的序列化和缓存同步问题;第三,答辩时讲数据库设计的完整性更容易展开。
购物车表结构很简单但加数量这个逻辑要写严谨:
public void addToCart(CartDTO dto, Long userId) { // 先查购物车表中是否存在相同SKU的记录 Cart cart = this.getOne(new LambdaQueryWrapper<Cart>() .eq(Cart::getUserId, userId) .eq(Cart::getSkuId, dto.getSkuId())); if (cart == null) { // 不存在就新建,注意把商品名称、主图、单价冗余进来 Cart newCart = new Cart(); newCart.setUserId(userId); newCart.setSkuId(dto.getSkuId()); newCart.setProductId(dto.getProductId()); newCart.setProductName(dto.getProductName()); newCart.setProductImage(dto.getProductImage()); newCart.setSpecName(dto.getSpecName()); newCart.setPrice(dto.getPrice()); newCart.setQuantity(dto.getQuantity()); this.save(newCart); } else { // 已存在则累加数量,但累加前检查库存上限 int newQuantity = cart.getQuantity() + dto.getQuantity(); if (newQuantity > dto.getStock()) { throw new BusinessException("库存不足"); } cart.setQuantity(newQuantity); this.updateById(cart); } }这里有个很容易出bug的地方:加购时校验库存,但真正防超卖要放到下单扣库存时做。因为用户加购后可能放一晚上再去结算,这期间商品可能被其他用户买完。所以加购校验只能是"友好提示",不能当作并发安全屏障。
5. 订单流程设计:从结算到支付的完整状态机
5.1 提交订单时的事务处理与库存扣减
用户从购物车勾选商品点"去结算",前端提交的是一次结算的商品明细列表。后端createOrder接口的完整步骤如下:
- 校验用户登录状态。
- 遍历提交的商品项,逐个查询最新价格和库存。
- 重新计算订单总金额——绝不相信前端传过来的总价,后端必须用数据库里的最新价格重新算。这个是对电商安全性的基本认知。
- 扣减SKU库存,同时检查扣减后不能为负数。
- 创建订单主记录(状态为0待付款)。
- 创建订单明细记录。
- 清空购物车中已结算的条目。
这7个步骤必须放在同一个事务里。更精确地说,第4步是并发控制的关键点:
@Transactional public Long createOrder(OrderCreateDTO dto, Long userId) { // 先锁定库存: 乐观锁或悲观锁 int updated = skuMapper.deductStock(dto.getSkuId(), dto.getQuantity()); if (updated == 0) { throw new BusinessException("库存不足或竞争失败"); } // 继续创建订单、明细... }deductStock的SQL类似UPDATE product_sku SET stock = stock - #{quantity} WHERE id = #{skuId} AND stock >= #{quantity}。这种"条件更新"是避免超卖的核心技巧——数据库行锁保证并发下只有一个事务能成功扣减库存。如果你用"先查库存再更新"的两步操作,并发场景下必然超卖,这个在技术评审时几乎必问。
事务里还要注意:分布式事务在单库单应用场景下不必要,@Transactional配合InnoDB的ACID已经够用。答辩被问到"为什么不用分布式事务"时,可以这样回答:当前系统是单体架构,数据库事务已经能保证一致性;将来拆微服务再引入分布式事务方案,比如Seata。
5.2 模拟支付:接入支付宝沙箱还是自己写一个?
毕设这一块通常有两条路。接入支付宝沙箱环境是真实感最强的选择,官方文档比较完善,也有专门的alipay-sdk-java依赖。但它的代价是配置繁琐——需要去开放平台签约沙箱应用、配置公私钥、回调地址。很多同学在这一步卡了几天,相当影响进度。我自己的建议是:第一条路用模拟支付,第二条路把支付宝沙箱作为加分项。
模拟支付接口的逻辑很简单:前端调/pay/mock接口,传订单号;后端直接判断订单状态是否为"待付款",如果是则把订单状态改为"待发货",写入支付时间和支付方式:
public void mockPay(Long orderId, Long userId) { Order order = orderMapper.selectById(orderId); // 校验归属权 if (!order.getUserId().equals(userId)) { throw new BusinessException("无权操作该订单"); } if (order.getStatus() != 0) { throw new BusinessException("订单状态不允许支付"); } order.setStatus(1); // 待发货 order.setPayTime(LocalDateTime.now()); orderMapper.updateById(order); // 同步更新商品销量,这里注意销量更新必须与订单状态变更在同一事务 orderItem list... }答辩演示时点击"立即支付"就能看到状态流转,用户体验非常完整。如果时间充裕,再在订单详情显示"模拟支付完成"提示,效果已经足够好。
真正要用心思的是订单超时取消逻辑。用@Scheduled(cron = "0 */2 * * * ?")每两分钟扫一次orders表,把status = 0 AND create_time < NOW() - INTERVAL 30 MINUTE的订单找出来,逐条取消并恢复库存。注意:定时任务执行时也要加事务,因为"取消订单"与"回补库存"必须是原子的。为了防止任务重复执行(比如集群部署环境下),可以加一个Redis分布式锁,毕设阶段说明这个思路即可,不必复杂实现。
5.3 购物车表与订单明细表的数据一致性
涉及购物车和订单的数据同步,一个常见的坑是:用户结算后清空购物车,此时删除条件必须与订单明细精确匹配。假设用户勾选了A和B两个商品去结算,但购物车里还有C,那么删除时只能删A和B对应的记录。用IN条件逐条删除即可,记得加userId条件防止删错人。
另一个值得提前处理的边界:结算中商品被管理员下架或删除。后端校验时如果发现商品不存在或状态不为1,应该返回明确提示"商品已失效,请刷新购物车",而不是直接报500。这种提示信息虽然简单,但能体现系统设计的细节水平,也是很多答辩评审会点出来的"用户体验"问题。
6. 后台管理端实现:商品管理、订单处理与数据分析
6.1 后台管理模块的权限控制
后台和前端需要分开处理。简单做法是用户表加userType字段:0普通用户、1管理员。后台所有接口统一加@RequireAdmin的拦截器校验。
在Spring Security的配置里,按路径区分权限是最常规的:
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/api/**").permitAll() .and().csrf().disable(); }这里要注意的是,JWT过滤器和Spring Security的过滤器链要正确协同。JWT过滤器从请求里解析Token并设置SecurityContext,Spring Security后续的hasRole判断才能拿到用户权限信息。
6.2 商品管理:图片上传的本地存储与访问映射
土特产商品管理最常出问题的是图片上传。本地存储方案要注意三点:
第一,上传的物理路径不能直接写在代码里,要配到application.yml:
upload: local: path: E:/project/upload/ # 存储目录 prefix: /upload/ # 访问前缀第二,启动类里加静态资源映射,使http://localhost:8080/upload/**能指向本地目录。写一个WebMvcConfigurer的Bean,重写addResourceHandlers方法。
第三,上传接口校验文件的扩展名(.jpg .png .gif)和大小(限制单图不超过5MB)。用UUID重新生成文件名,避免中文文件名带来的编码问题:
String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFileName = UUID.randomUUID().toString() + ext; file.transferTo(new File(uploadPath, newFileName).getCanonicalFile());如果项目部署在云服务器,更推荐接入对象存储OSS,只需要引入SDK、配置Bucket和AccessKey,代码量并不大,且能展示你对生产级方案的理解。答辩时提一句"本地存储适合演示环境,生产环境建议用对象存储",就很加分。
6.3 订单管理:状态筛选与发货操作
后台订单管理页面有四个核心操作:
- 按状态筛选:待付款、待发货、待收货、已完成、已取消。用枚举值和下拉框筛选。
- 发货:录入物流公司和运单号,订单状态从"待发货"变为"待收货"。
- 查看详情:展示订单基本信息 + 明细列表 + 收货地址 + 操作日志。
- 取消订单:后台强制取消待付款或待发货状态订单,同时回补库存。
发货操作中,订单表需要预留logistics_company和logistics_no两个字段。这些字段虽小,但关系到"待收货"状态能否正常流转,也是评委容易提问的点。
发货后用户端"确认收货"操作,主体是用户点击按钮,后端校验订单归属和状态(必须是待收货),然后改为"已完成",同时记录receive_time。这种状态只能单向前进的设计(不能从已完成回退到待收货)要在Service层做状态机校验,防止用户模拟请求篡改数据。
6.4 简单的数据看板:撑起"数据分析"这个加分项
后台首页如果只有商品和订单表格,会显得比较单薄。加一个数据看板Top区域:销售总额、今日订单数、待发货数量、商品总库存。再整合两个图表:近7日销售额折线图、商品分类销量饼图。
数据看板中最核心的SQL是按日期统计销售金额。没有订单表时可以先这样说数据库结构:
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS date, SUM(total_amount) AS total_amount FROM orders WHERE status IN (1, 2, 3) -- 已支付状态 AND create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') ORDER BY date ASC;然后前端用ECharts折线图展示。这块不需要太复杂,但能把"数据库聚合查询 + 前端可视化"两个能力展示出来。答辩时如果被问到性能问题,可以说"数据量大了以后,这类统计可以迁移到离线计算或数仓,目前这个规模用MySQL聚合足够"。
7. 前端页面与交互:Vue页面怎么搭、请求封装怎么做
7.1 页面结构与路由规划
如果采用前后端分离方案,前端推荐用Vue 2.7 + Element UI(更容易上手)或Vue 3 + Element Plus(更现代)。毕设的功能页面其实不多,核心路由可以这样规划:
| 路由 | 页面 | 说明 |
|---|---|---|
/ | Home.vue | 首页:轮播图、推荐商品 |
/product/:id | ProductDetail.vue | 商品详情:SKU选择、加购按钮 |
/cart | Cart.vue | 购物车:改数量、删除、全选、结算 |
/order/confirm | OrderConfirm.vue | 订单确认:地址选择、商品清单 |
/order/list | OrderList.vue | 我的订单:状态Tab、各状态列表 |
/order/detail/:id | OrderDetail.vue | 订单详情 |
/admin | AdminLayout.vue | 后台布局:侧边栏+内容区 |
/admin/product | AdminProduct.vue | 商品管理列表 |
/admin/product/edit | AdminProductEdit.vue | 商品新增/编辑 |
/admin/order | AdminOrder.vue | 订单管理 |
路由层级不用太深,能通过功能导航到达所有页面即可。注意点只有一个:后台路由要加全局守卫,判断本地存储的userType是否为管理员,否则跳回登录页。
7.2 Axios请求封装与Token刷新
前端请求封装直接决定你全站接口调试的效率。我建议按以下方式统一封装:
// request.js import axios from 'axios' const service = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器: 自动带Token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) // 响应拦截器: 统一处理业务码和401 service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { this.$message.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { // Token失效: 清空并跳回登录页 localStorage.clear() location.href = '/login' } return Promise.reject(error) } )如果时间够,再加一层Token刷新机制也是加分项——后端额外提供一个/refresh接口,用没过期的RefreshToken换新AccessToken。不过对毕设而言定时任务和消息处理已经很重了的话,先做统一封装即可。
7.3 商品详情页的SKU选择交互
商品详情页是整个前端交互中最容易做砸的。核心逻辑是:用户点击不同规格时,展示对应SKU的价格、库存、图片。你需要维护一个currentSkuId变量,规格选择后用它的属性重新渲染价格区域和"库存不足"提示。
这里有个常见做法值得借鉴:先把规格组数据做一个映射。接口返回的SKU列表往往是扁平数组,前端根据specName拆成规格组。当所有规格都被选中时,用组合键去匹配完整SKU。如果匹配不到说明该组合无货,禁用"加入购物车"按钮。实现不算复杂,但能让答辩评委看到前端逻辑的完整性。
8. 毕设避坑实录:我在帮同学们调试过程中遇到的高频问题
这个部分我想专门写一下,因为这几个月帮不同同学复现这个题目,我整理出了几个反复出现的共性问题,提前注意到能省非常多的时间。
8.1 跨域请求配置:为什么前端请求一直报403/404
前后端分离开发时,如果前端跑在localhost:8081,后端在localhost:8080,必然存在跨域问题。处理方案是后端写一个CORS配置类:
@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); } }最容易踩的坑:allowCredentials(true)时allowedOrigins不能设为*,必须用allowedOriginPatterns。否则Spring Boot直接拒绝请求。另外,如果用了Spring Security,CORS配置写在这种地方可能不生效——因为Security的过滤器链先拦截了请求,你得在SecurityConfig里同时配置.cors()并注入这个CorsConfigurationSource。这个"双配置"问题的排查过程我印象非常深,当时某位同学前端一直报跨域错误,我检查了一小时才发现是Security过滤链优先级导致的。
8.2 MyBatis-Plus的自动填充与逻辑删除
土特产商品有create_time和update_time两个字段,每次手动setCreateTime很繁琐。用MyBatis-Plus的自动填充功能更优雅:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }实体类对应字段上加@TableField(fill = FieldFill.INSERT)和@TableField(fill = FieldFill.INSERT_UPDATE)。
另一个必须处理的是逻辑删除。后台删除商品如果直接删数据库行,用户历史订单里关联的数据就乱了。正确做法是在商品表加deleted字段(0未删除、1已删除),MyBatis-Plus实体类加@TableLogic注解,所有查询自动带上deleted = 0条件。这样数据永远在库里,只是不出现在商品列表。
删除商品时还要做个级联处理:把该商品关联的SKU也逻辑删除,同时若有未完成订单引用该商品,应当保留订单快照逻辑,不能把订单明细也删掉。
8.3 接口返回格式不统一导致的联调痛苦
很多同学第一次做前后端分离时,今天这个接口返回{code: 200, data: []},明天那个接口返回{"status":0},前端封装很难统一处理。建议从第一天就定好统一返回对象:
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }配合一个全局异常处理器@RestControllerAdvice,把BusinessException和兜底异常都转成这个格式。这样前端响应拦截器只要判断code是否等于200,流程瞬间清爽。
8.4 前端表格分页参数名对不上
后端Page对象默认接收的参数名是current和size,而前端表格组件默认传的是pageNum和pageSize。如果不对齐,分页永远只有第一页数据。两种解决办法:要么后端在Controller手动映射:
@GetMapping("/list") public Result<Page<Product>> list( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) { Page<Product> page = new Page<>(pageNum, pageSize); // ... }要么前端axios拦截器统一改参数名。更建议在方法上使用@RequestParam显式映射,因为后端能同时兼容两种入参,也方便前端对接。
9. 部署发布、答辩要点与后续扩展建议
9.1 打包与部署需要注意的事项
Spring Boot打包直接用Maven的package生成jar包,但毕业设计部署时有三件事很多人会栽跟头:
数据库初始化:本地MySQL的表结构和初始数据要导出SQL脚本,部署到服务器后先执行脚本。脚本里要包含一个管理员账号、几个分类、十几条演示商品和SKU数据。演示不能让人登录后看到空荡荡的界面。
配置外部化:数据库地址、端口、Redis地址不要硬编码,打在application-prod.yml里,通过--spring.profiles.active=prod激活。这个方法很简单,却是生产环境必备。
静态文件路径:本地upload.path可能是E:/xxx,部署到Linux服务器就要改成/usr/local/upload/。分享时提醒一下,路径分隔符问题经常被忽略,Windows用反斜杠,Linux用正斜杠,用File.separator或Path API拼接可以避免跨平台问题。
9.2 答辩时的高频提问与应答思路
根据我帮同学们模拟答辩的经验,评委最喜欢围绕这几个点发问:
"为什么选择Spring Boot而非Spring MVC?"回答要点:Spring Boot在Spring MVC基础上提供了自动配置、起步依赖、内嵌Tomcat,让开发者专注于业务代码,显著减少XML配置;同时它本身并没有抛弃MVC分层思想,底层依然是Spring的IoC和AOP。
"库存超卖问题怎么解决的?"回答要点:在数据库层面使用条件更新WHERE stock >= quantity,配合事务保证原子性。还可以补充:更高并发场景可以用Redis预扣库存 + 异步落库、分布式锁或消息队列削峰。表明你既知道当前方案的适用边界,也知道演进路径。
"JWT相比Session有什么优缺点?"回答要点:JWT无状态、适合分布式和前后端分离、可以携带自定义声明。缺点是服务端不能主动让Token失效(除非引入黑名单),Token体积比SessionID大。这个问题的辩证回答比单方面吹捧更能体现理解深度。
"为什么商品规格要单独建一张表?"回答要点:一个商品可能有多个不同的价格库存组合,属性维度不同导致一对多关系,若把多规格塞进商品表会增加冗余和更新异常风险;SKU表设计也能与主流电商系统保持一致。
"定时任务超时取消订单会重复执行吗?"回答要点:单点部署不会有问题;集群部署时定时任务可能被多个节点同时触发,需要引入分布式锁(如Redis SetNX)保证同一时间段只有一个节点执行。
9.3 这个题目还能怎么玩出差异化
如果班级里选这个题的人比较多,你需要给自己的作品加一点差异化亮点。下面几个方向我认为都比较适合在毕设评审中讲清楚:
方向一:增加多级分类。土特产往往有"按产地"和"按品类"两种分类方式。可以设计两级分类:省级产地 + 具体品类。首页支持按产地筛选,这是区别于普通电商的垂直特色。
方向二:秒杀功能。选择某个SKU做限时低价,用Redis预减库存 + 数据库最终扣减实现秒杀。技术含量高,但做好需要一定时间,不建议毕设期间临阵尝试,除非时间充足。
方向三:物流轨迹查询对接。接入快递鸟或模拟物流轨迹,给订单增加"物流轨迹时间线"。这个能让用户端体验感强很多,还能展示你对第三方API对接的熟悉程度。
方向四:会员积分体系。用户下单获得积分,积分可在下次购买时抵扣。积分流水和抵扣计算的实现相对独立,不容易破坏核心订单流程,是很稳的加分模块。
方向五:消息通知。用户下单后给微信模板消息或站内信通知。如果不想接第三方推送,在站内实现一个"未读消息列表"就足够展示消息推送思路。
10. 几个值得重点保留的个人经验分享
帮不同同学把这份题目从零搭到能跑通、能答辩,我最后想浓缩几条最有价值的心得,这几条我相信即便去真实电商公司做后端开发,也一样适用:
第一条,先画业务流程图再动手写代码。我见过太多同学上来就建表,建到一半发现订单状态要对不上,又一删了之。把"注册→浏览→加购→下单→支付→发货→收货→售后"这条主链路画出来,每一步的状态和数据流都清楚了,代码写起来就是按图索骥。
第二条,以"写生产级代码"的心态写毕设代码。所有的用户输入都可能是恶意输入,所有的接口都可能被越权调用。校验参数、校验状态、校验归属权,这三件事做踏实了,系统质量会有质的飞跃。后台管理接口尤其要小心,如果一个普通用户能通过修改请求路径访问到后台删除商品接口,这个系统就有严重漏洞。
第三条,千万别只依赖后端403错误提示来判断问题。前后端联调时,先把浏览器的Network面板打开,看请求是否真的发出去了、状态码是什么、响应体是什么,再看控制台报错。大多数问题都是"路径404"或"参数名不一致"这种小问题,不要一上来就怀疑Spring配置。
第四条,资料查阅顺序建议排优先级。遇到问题首选官方文档和官方示例代码,其次看GitHub上star高的开源电商项目,再看社区博客。优先参考最可靠的信息源,能少走很多弯路——这不是客套话,是真有这方面的教训。某次排查一个JWT解析编码问题,翻遍了帖子都没头绪,两小时后再查官方手册才发现是HmacSHA256签名验证Key长度受版本影响。
如果你也要做"基于Spring Boot的电商平台"这类题目,希望这篇文章里提到的方案、代码和踩坑经历能帮你少走几周弯路。这些设计与实现思路不是单一项目的答案,也可以迁移到其他垂直电商、校园二手交易平台、企业商品管理系统等场景。把这些基础能力搭通了,后面做什么题目都会顺很多。