SpringBoot家乡特产商城:从购物车到订单闭环的毕设实战
1. 为什么推荐这个题目以及它到底在做什么每年到了毕设选题季总有不少同学在 SpringBoot 系列的题目里反复纠结。有的选图书管理系统有的选校园二手交易平台说实话这些题目本身没什么问题但如果你想要一个业务链路完整、有亮点可讲、答辩时能展示出工程思维的题目家乡特产销售系统是一个非常值得投入的方向。先把这个系统到底做什么讲清楚。它不是一个简单的商品列表加购物车而是一个完整闭环的线上营销平台用户端能看到特产分类、商品详情、库存状态、下单购买管理端能维护商品上下架、处理订单状态、管理分类和轮播图。中间还涉及购物车、收货地址、订单状态流转、库存扣减、支付回调对接等一系列电商系统的核心机制。为什么说这个题目适合作为毕设因为它的业务复杂度卡在一个非常合适的位置——比单纯的 CRUD 管理系统高半档但又没到真正商用电商平台那种需要分布式事务、消息队列的程度。用 SpringBoot 单体架构加上合适的设计模式完全可以驾驭又能展示出你对业务建模和系统设计的能力。选题背后的另一个实际考虑是家乡特产这个业务定位带来的优势。特产商品天然具备图片丰富、分类明确、故事性强等特点系统做出来的界面演示效果特别好。答辩时你甚至可以结合自己家乡的具体特产比如某地的茶叶、果干、手工艺品来做演示数据比商品A商品B这种抽象命名有说服力得多。这个题目适合谁来参考如果你是 Java 后端方向的应届毕业生SpringBoot 已经基本入门希望做一个能从数据库设计到前端页面完整落地的项目那这篇文章提到的架构思路和核心代码都可以直接借鉴。如果你对前端不太熟悉也可以采用服务端渲染加模板引擎的方案避免前后端分离带来的跨域和部署复杂度。2. 技术选型与整体架构SpringBoot 之外的几个关键决策2.1 为什么用 SpringBoot 而不是 SSH 或其他框架SpringBoot 成为 Java 毕设主流选择的原因很实际自动配置省去了大量 XML 配置内置 Tomcat 让部署变得极其简单生态成熟到任何你需要的功能都能找到现成的 Starter。对于特产销售系统这个场景SpringBoot 可以提供Spring MVC 处理 RESTful 接口和页面请求Spring Data JPA 或 MyBatis 处理数据持久化Spring Validation 做参数校验Spring Security可选做登录认证内置定时任务能力处理订单超时关闭从学习价值的角度看SpringBoot 并没有把底层原理完全屏蔽——你依然需要理解依赖注入、AOP、事务管理这些核心概念只是不需要手工拼装 XML 了。这对毕设来说是最好的平衡点。2.2 前端方案前后端分离还是服务端渲染这个决策会直接影响你的开发效率和演示效果。我给你的建议是优先考虑服务端渲染使用 Thymeleaf 模板引擎原因有两点第一毕设项目最怕的是技术栈铺得太散。前端用 Vue 或 React意味着要额外处理 Node.js 环境、跨域配置、打包部署问题。一旦某个环节出问题排查成本会翻倍。而 Thymeleaf 直接在 SpringBoot 项目里写 HTML 页面数据用th:each、th:text指令渲染对一个后端为主的毕设来说已经足够。第二电商类页面的 SEO 和首屏展示服务端渲染天然占优势。特产展示页面的核心诉求是让用户快速看到商品图片和描述服务端渲染不需要等待接口加载再拼接 DOM体验上更接近真实商城。如果你确实对前端更熟悉也可以做前后端分离但请务必在项目文档里写清楚接口文档格式、跨域处理方案、前端构建产物如何部署。这些内容是答辩时的加分项。2.3 数据库选择与核心表设计MySQL 是这个场景最稳妥的选择InnoDB 引擎支持事务和外键约束符合电商订单系统的数据一致性要求。数据库命名尽量语义化表名前缀可以区分业务模块模块表名核心字段用户t_userid, username, password, phone, avatar, address分类t_categoryid, name, sort, icon商品t_productid, category_id, name, subtitle, main_image, detail, price, stock, sales, status轮播图t_bannerid, image, link_url, sort购物车t_cartid, user_id, product_id, quantity, checked订单t_orderid, order_no, user_id, total_amount, status, create_time, pay_time订单项t_order_itemid, order_id, product_id, product_name, product_image, price, quantity收货地址t_addressid, user_id, receiver, phone, province, city, district, detail, is_default订单和订单项拆分是电商系统的经典设计因为一个订单可能包含多个商品而每个商品在生成订单瞬间的价格、名称、图片都要快照保存不能关联查询商品表实时获取——否则商品改价后历史订单金额就乱了。这个细节是很多毕设容易忽略但在答辩中极容易被打听的点。2.4 工程目录的模块化组织一个好的包结构能让代码评审老师一眼看出你的设计能力。我推荐按业务模块分包而不是按技术类型分包com.example.townspecial ├── common // 通用工具、统一返回结果、异常处理 ├── config // 配置类拦截器、WebMvc、文件上传 ├── controller // 控制层 ├── service // 业务层接口与实现 ├── mapper // 数据访问层 ├── entity // 实体类 ├── dto // 数据传输对象 ├── vo // 视图对象 └── constant // 常量定义如订单状态枚举按业务模块分包在大型项目中有争议但在毕设规模下按技术层次分包反而更容易让每个类的职责清晰。controller 层只负责接收参数和返回结果service 层处理业务逻辑mapper 层只做数据读写每一层之间通过接口依赖这样在写文档时也非常容易画分层架构图。3. 特产展示模块的完整实现链路3.1 从商品实体到分类检索的数据库操作商品展示是整个平台的门面。用户进入系统后第一步就是浏览特产所以这个模块的体验直接决定你对这个项目的印象分。先看商品实体类。使用 MyBatis-Plus 的话实体类配合注解可以大幅减少重复代码Data TableName(t_product) public class Product { TableId(type IdType.AUTO) private Long id; private Long categoryId; private String name; private String subtitle; private String mainImage; private String detail; private BigDecimal price; private Integer stock; private Integer sales; private Integer status; // 0-下架 1-上架 private Date createTime; private Date updateTime; }这里有一个容易被忽略的点detail字段如果存放纯文本那商品详情页就只是一堆文字展示效果非常干瘪。更推荐的做法是存放HTML 片段后台维护商品时用富文本编辑器生成内容前端详情页直接渲染。这样你可以把产品故事、产地信息、食用方法用图文排版的方式呈现出来视觉效果好很多。分类检索的核心是一个简单的条件查询。用户点击某个分类时前端传categoryId和pageNum后端通过 LambdaQueryWrapper 构造查询条件再配合 MyBatis-Plus 的分页插件完成分页返回public PageResultProductVO queryProductPage(Long categoryId, Integer pageNum, Integer pageSize) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(Product::getStatus, 1) .eq(categoryId ! null, Product::getCategoryId, categoryId) .orderByDesc(Product::getSales); PageProduct page productMapper.selectPage(new Page(pageNum, pageSize), wrapper); // 转 VO 并补充需要展示的冗余字段 }3.2 商品搜索别只用 LIKE试试带权重的关键词匹配特产商品有一个特点用户往往记不住确切的商品名更多是搜坚果点心腌制品这种品类词。如果只是WHERE name LIKE %关键词%召回率和相关性都有限。一个轻量级但效果明显的方案是给商品增加搜索关键词字段。在商品表增加一列search_keywords后台维护商品时把可能被搜索的词都填进去比如商品名是黄山烧饼搜索关键词可以是烧饼,酥饼,梅干菜,安徽特产。查询时对name和search_keywords做 LIKE 匹配再用条件判断提高精确匹配的优先级wrapper.and(w - w.like(Product::getName, keyword) .or().like(Product::getSearchKeywords, keyword)) .orderByDesc(Product::getSales);这个方案不需要引入 Elasticsearch 这样的重型中间件但对毕设的场景足够用而且你在文档里写清楚设计思路——通过扩展搜索字段提升召回率用销量排序保证相关性——本身就是答辩的亮点。3.3 图片存储本地目录上传是最稳的方案特产系统对图片的需求量很大轮播图、商品主图、详情页图片。毕设阶段不要引入云存储OSS、COS原因很简单云存储需要开通服务、配置密钥、涉及费用而且答辩演示时万一网络不好图片加载不出来是很尴尬的事。本地存储的方案是配置一个上传路径图片保存到服务器某个目录然后通过映射把该目录暴露为静态资源访问。Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Value(${file.access-path}) private String accessPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 将访问路径映射到本地磁盘目录 registry.addResourceHandler(accessPath **) .addResourceHandler(file: uploadDir); } }对应地在配置文件里指定路径并确保启动时目录存在file: upload-dir: ./upload/ access-path: /upload/上传接口用 MultipartFile 接收文件校验文件类型jpg/png/webp和大小限制在 2MB 以内比较合理文件名用 UUID 重新生成避免中文名和重复名导致的访问问题。注意如果你计划打包成 jar 部署图片目录不要放到 jar 包内部。单独创建一个外部目录作为上传路径否则每次重新部署都会丢失用户上传的文件。这个问题在实际开发中出现频率非常高。4. 购物车与订单一体化管理把核心流程拧成一条线4.1 购物车的两种实现思路购物车功能看起来简单实际上设计时有两个方向未登录状态用 Cookie 存储登录状态用数据库存储。考虑到毕设的场景我建议直接做数据库存储因为这样实现简单而且能展示出你对状态管理的理解。购物车表的核心字段是user_id, product_id, quantity, checked。添加购物车时先查是否已存在该用户的这条商品记录存在则数量加一不存在则新建。这里有一个隐藏的问题购物车里的商品数量不能超过库存所以每次加入前要检查库存。有些同学会忽略这一步导致最终下单时发现库存不足体验很差。我建议在 Service 层暴露一个专门的方法来处理加购逻辑Transactional public ResultVoid addToCart(Long userId, Long productId, Integer quantity) { Product product productMapper.selectById(productId); // 校验商品状态 if (product null || product.getStatus() ! 1) { return Result.error(商品不存在或已下架); } // 校验库存 Cart cart cartMapper.selectOne(new LambdaQueryWrapperCart() .eq(Cart::getUserId, userId) .eq(Cart::getProductId, productId)); int newQuantity (cart null ? 0 : cart.getQuantity()) quantity; if (newQuantity product.getStock()) { return Result.error(库存不足当前库存仅剩 product.getStock() 件); } // 新增或更新 }4.2 订单生成的事务边界为什么不能只加事务注解从购物车生成订单是整个系统最核心的一段逻辑。表面上就是两个操作插入订单记录、插入订单项记录但实际要考虑的事情比想象中多。第一步把购物车中选中的商品查询出来依次校验上下架状态和库存。这里要注意商品价格要从数据库实时读取不能信任前端传来的价格否则用户可以篡改请求实现低价购买。第二步计算商品总金额。特产销售系统涉及邮费这里建议用一个简单的规则满 99 元包邮否则邮费 10 元。虽然规则简单但你需要在代码里把它做成一个独立的方法而不是在 Controller 里临时计算这样后续要调整规则就很方便。第三步生成订单号。订单号的格式建议为日期时间 随机数例如20250507153000123456。不要用数据库自增主键作为订单号因为订单号需要在用户支付时暴露出来且要具备一定的不可猜测性。第四步扣减库存。这里有个非常经典的并发问题两个用户同时下单同一件商品如果库存只有 3 件一个买 2 件一个买 2 件理论上只能有一单成功但并发情况下可能双双扣减成功导致超卖。解决超卖最简洁的方式是乐观锁扣减用 MySQL 的更新语句加上库存条件UPDATE t_product SET stock stock - #{quantity}, sales sales #{quantity} WHERE id #{productId} AND stock #{quantity}通过判断更新影响行数来确定是否扣减成功。如果影响行数为 0说明库存不足需要回滚当前订单创建流程。第五步在事务注解Transactional包裹下完成订单插入、订单项批量插入、购物车清空、库存扣减。注意事务边界里不应该包含支付等待等耗时操作数据库连接池的资源很宝贵长事务会影响吞吐量。4.3 订单状态机用枚举常量替代硬编码数字订单状态是面试和答辩中高频出现的知识点。很多同学的代码里直接写status 1、status 2加数字魔法的代码后期改起来非常痛苦。更规范的做法是定义订单状态的枚举常量public enum OrderStatus { PENDING_PAYMENT(0, 待付款), PENDING_SHIPMENT(1, 待发货), SHIPPED(2, 已发货), COMPLETED(3, 已完成), CANCELLED(4, 已取消), REFUNDING(5, 退款中); private final int code; private final String desc; }订单状态的流转要控制在 Service 层不要在 Controller 里随意修改。例如取消订单时只有待付款状态的订单能直接取消已发货的订单需要走售后流程。你在写业务代码时把这些规则明确下来让状态流转的调用关系清晰可见这让你的系统像一个设计过的系统而不是一堆接口的堆砌。4.4 订单超时关单的实现定时任务还是延迟队列真实的电商系统遇到用户下单后不支付需要在一定时间内自动关闭订单释放库存。毕设阶段用Scheduled定时任务是更实际的选择不要按照网上的教程引入 RabbitMQ 延迟队列——对于单体架构的毕设来说那是一个为了技术而技术的设计撑不起复杂度也容易在答辩时被问住。定时任务的思路比较直接每隔一段时间扫描订单表把创建时间超过 30 分钟且状态为待付款的订单批量关闭同时把订单里包含的商品库存回补。注意这里的库存回补不能直接stock quantity而是要精确到订单项的每个商品Scheduled(fixedDelay 60000) // 每 60 秒执行一次 public void closeExpiredOrders() { ListOrder expiredOrders orderMapper.selectExpiredOrders(30); for (Order order : expiredOrders) { // 先改状态再回补库存保证修改状态失败时不会错误回补 int updated orderMapper.updateStatusWithCheck(order.getId(), OrderStatus.PENDING_PAYMENT, OrderStatus.CANCELLED); if (updated 0) { // 遍历订单项逐件回补库存 } } }这里最容易被忽略的是重复执行问题定时任务执行过程中如果用户刚好支付了这笔订单任务又把这笔订单关闭了怎么办解决方式很简单状态更新用条件更新WHERE id? AND status0只有当订单还是待付款状态时才能被关闭否则跳过。这一句 SQL 就是乐观锁思想的体现。5. 如何用一个模拟支付安全走通全流程5.1 支付环节的降级处理真实接入支付宝或微信支付需要企业资质、应用审核等流程学校答辩环境里也基本不可能演示真实支付。合理的做法是做一个模拟支付的过渡方案用户点击确认支付后前端弹出收银台窗口展示订单金额和支付方式选择支付方式后点击确认后端把订单状态从待付款改为待发货。模拟支付的实现并不复杂但要注意在代码层面预留好真实支付网关的扩展位置。可以定义一个PaymentService接口当前实现是MockPaymentServiceImpl如果未来要接入真实支付只需要新增一个AlipayPaymentServiceImpl通过策略模式切换。在毕业论文里把这个设计写清楚答辩老师会认为你有面向扩展的设计意识。5.2 支付模块的幂等处理模拟支付同样要处理重复回调问题——用户连续点击支付按钮订单状态不能被覆盖。处理方式是在更新语句上增加状态条件public ResultVoid mockPay(Long orderId, Long userId) { int updated orderMapper.updateStatus(orderId, userId, OrderStatus.PENDING_PAYMENT.getCode(), OrderStatus.PENDING_SHIPMENT.getCode()); if (updated 0) { return Result.error(订单状态异常请刷新后重试); } return Result.success(); }用户只能对待付款状态的订单发起支付一旦已支付再点按钮更新影响行数为 0 就会返回提示不会产生重复处理。6. 管理后台与用户端的关键功能对照6.1 用户端功能落点用户端的核心页面和功能可以梳理成一张清单开发时按顺序推进页面核心接口关键逻辑首页轮播图分类推荐商品推荐商品按销量降序取前 8 件商品列表分页分类筛选点击分类切换URL 保持参数商品详情商品信息详情图展示富文本 detail 字段购物车增删改查勾选商品同步校验库存勾选状态持久化到数据库下单页创建订单选择地址展示商品清单、邮费规则、应付总额个人中心订单列表地址管理按状态Tab切换显示不同操作按钮需要特别提醒的是收货地址管理。地址信息作为订单的附属数据在生成订单时应当将地址内容快照到订单表或单独的快递信息表里。用户后续修改地址不应该影响已下单的订单配送信息。这个历史数据不可变的设计意识是电商系统中订单模块的基本要求。6.2 管理端功能落点管理端的功能目标是让运营者可以脱离数据库修改网站内容。下面这几个模块是必须完成的商品管理新增/编辑/上下架商品维护主图、详情、价格、库存、分类。分类管理维护一级分类调整展示顺序。订单管理查看全部订单按状态筛选执行发货操作填写物流单号将订单状态从待发货改为已发货。轮播图管理维护首页轮播图片及跳转链接。管理端的权限控制建议用最简单的方案管理员表中存一个role字段登录时写入 Session。拦截器对/admin/**路径做校验未登录或非管理员访问时直接重定向到登录页。不要引入 Spring Security 的完整框架因为权限模型的复杂度远超毕设需求配置不当反而容易让你陷入 debug 泥潭。7. 毕设开发中的高频坑与排查思路7.1 图片上传成功但页面显示 404这个问题在本地开发时极其常见原因通常有两个第一上传目录没有创建SpringBoot 不会自动创建file.upload-dir指向的目录第二静态资源映射路径写错比如访问路径是/upload/**映射配置却写成了file:upload/少了斜杠或路径对不上。排查时先看浏览器控制台图片请求的完整 URL 和状态码再用文件管理器确认上传目录下是否真的生成了文件。如果文件存在但访问 404几乎可以确定是映射配置的问题。7.2 分页查询排序字段无效MyBatis-Plus 的orderByDesc(Product::getSales)如果字段名和数据库列名映射不一致生成的 SQL 可能不是你想的那个排序。在你用了TableName和驼峰映射的基础上检查配置map-underscore-to-camel-case是否开启以及实体字段的命名和表字段是否严格对应。sales这个字段很容易被写成sell、saleCount这种名称一旦不一致排序的 lambda 引用根本编译不过去反而比运行时报错更容易发现。7.3 订单状态改了但用户看不到变化订单列表出现状态未刷新的错觉大多数情况不是数据没更新而是前端页面做了缓存。如果你用 Thymeleaf 做服务端渲染浏览器会缓存部分静态资源如果是前后端分离要考虑接口返回的订单列表是否按更新时间排序。建议在订单列表查询时统一ORDER BY create_time DESC让新订单和状态变更后的订单稳定出现在最前面。7.4 端口被占用导致项目启动失败开发过程中频繁启动项目有时候上一次运行的进程没有完全退出就会出现 Port 8080 was already in use 的报错。排查方式Windows 用netstat -ano | findstr 8080查占用 PID再在任务管理器中结束对应进程。这个坑虽然简单但在答辩现场一旦出现会非常影响节奏建议提前把启动方式整理成文档让项目在干净环境下也能一键跑起来。8. 答辩演示的准备建议和代码之外的加分项答辩时不要只讲我做了什么功能而是讲我为什么这样做、遇到了什么问题、怎么解决的。准备一个5 分钟左右的演示脚本演示路径按照用户浏览 → 加购 → 下单 → 支付 → 后台发货 → 用户确认收货的完整链路来走让顺序和你 PPT 里的业务流程图完全一致。演示时重点展示下面几个细节这些通常是把分拉高的关键点下单后故意不支付等定时任务扫描后自动关闭订单并回补库存。把商品库存修改为 1让两个账号同时下单同一件商品展示超卖拦截效果。在管理后台修改商品价格和库存前端商品详情页立即同步变化。演示上传图片功能说明文件存储方案和路径映射配置。动手写这个系统前建议把建表 SQL 先想清楚先跑通用户注册登录 → 添加商品 → 下单 → 支付 → 发货这条主链路然后再补充管理端功能。把核心主链路先跑通你的心理压力会小很多后期加功能也只是往这个稳定的骨架上继续填充血肉。这个系统的复杂度作为毕设刚刚好既有电商业务的完整性又不会让你在技术深坑里耗尽时间做完之后你对 SpringBoot 的整体理解绝对会上一个台阶。