基于SpringBoot的山东特色农产品交易平台开发实战 做毕设选了这个“SpringBoot山东特色农产品交易平台”题目项目编号13935前后捣腾了小一个月从需求和原型梳理到代码落地还专门针对“农产品”这个品类的特殊性做了不少取舍。很多同学做电商系统喜欢直接照搬商城脚手架拿个通用模板改改名字就交差结果答辩的时候一被问“为什么这样设计”基本就卡住了。尤其“特色农产品”这个关键词和普通标准化商品完全不是一回事——蔬菜水果生鲜类目有季节性、有产地属性、有重量浮动、有保质期压力这些如果不落进数据库设计和订单流程里那这个平台就只是空壳。这篇分享就当带完全复现这个项目的流水账和核心设计笔记从需求分析、技术选型、数据库建模到核心功能实现、踩坑记录尽量说透适合正在做SpringBoot毕设、或者想快速上手一个可演示电商项目的人参考。1. 项目整体设计与需求拆解1.1 平台定位与业务角色这个平台不是一个纯粹的B2C商城。它要解决的核心场景是山东本地特色农产品从农户/合作社/经纪人手中经过线上展示、下单、结算、物流跟踪最终到达消费者手里。这个链路里有两个关键的业务角色——买家端和卖家端再加上一个平台管理端来审核上架、处理订单异常、做运营统计。买家端注册登录、浏览分类商品、按产地/季节筛选、加入购物车、下单结算、查看个人订单、模拟支付、确认收货、评价商品。卖家端店铺入驻申请、商品发布带产地、季节、品类标签、上下架管理、库存同步、处理待发货订单、查看营收明细。管理端用户与店铺审核、商品审核、分类管理、全量订单查询、数据概览热销商品、近7日成交、用户增长趋势。从毕设答辩的角度看这三个角色的划分已经够展示完整的权限控制和业务闭环了不用再做复杂的分销、多级代理之类的东西容易把项目拖垮。1.2 农产品交易场景的特殊性分析既然叫“特色农产品平台”就必须做出和普通商城不一样的逻辑这是后续所有功能设计的出发点。我梳理了四个必须考虑的点第一SKU的脆弱性。蔬菜水果类商品的称重包装不稳定同一品类可能出现“2斤装”和“2.5斤装”价格随行就市所以商品表不能完全套用“标准SKU”那套死板模型需要增加规格浮动描述和按重量计价的后台配置。第二季节与产地筛选。用户在逛平台时往往带着明确的地域偏好比如“我想买烟台苹果”“我要章丘大葱”。所以商品表里必须建立产地字段 季节标签的组合查询机制而不是只给一个简单的分类树。第三库存的时限性。普通电商库存可以长期稳定但农产品的库存往往是“今天有、明天未必有”且与采摘批次直接相关。因此设计了批次库存批次字段下单锁定库存、支付成功后扣减并在后台支持商家手动修正库存。第四订单履行链路长。农产品往往需要冷链/专线物流买家对物流状态的敏感度远超普通商品所以订单状态机里必须包含“发货”和“物流路径”的模拟哪怕毕设不接真实快递API也要把流程结构搭好。1.3 功能清单与范围控制明确落地到代码里的功能我整理成了一张功能清单表整体控制在“麻雀虽小、五脏俱全”的体量模块功能点备注用户认证注册、登录、JWT签发、个人信息查询密码BCrypt加密存储商品检索按名称模糊、按分类、按产地、按季节筛选支持多条件组合查询购物车加入、修改数量、删除、勾选结算基于用户维度存储订单流程创建订单、模拟支付、发货、确认收货、取消状态机流转超时取消卖家中心商品发布/编辑、库存调整、待发货订单列表与买家端分离管理后台用户管理、审核、分类管理、订单管理、统计独立角色访问系统支撑统一返回结果、全局异常、跨域配置、文件上传项目的基础设施范围控制很重要我当时差点加了优惠券、收藏、地址簿管理、秒杀、直播带货后来理性评估后把收藏合并到购物车逻辑里、地址簿用简版字段代替、优惠券和秒杀整个砍掉——一来是毕设周期撑不住二来是评审时不容易说清楚硬撑的复杂度反而成了扣分项。2. 技术选型与工程架构说明2.1 后端基础框架与ORM选型技术栈这一层没有悬念地选了SpringBoot 2.7.x MyBatis-Plus 3.5.x MySQL 8.0。SpringBoot 2.7是目前教学里最普遍、资料最多、和大部分毕业设计题目兼容性最好的版本比3.x稳妥——3.x虽然新但部分老依赖兼容要额外花时间趟坑。MyBatis-Plus比原生MyBatis更适合这个体量的项目核心价值在于不用手写大量重复的CRUD方法内置的LambdaQueryWrapper可以直接拼复杂条件比如多条件商品检索代码能精简不少而且它有分页插件管理端列表页的翻页非常省事。还有一点很实用MP的代码生成器可以把数据库里的每张表直接生成Entity、Mapper、Service、Controller毕设的前期搭架子效率能提升一个档次。持久层连接池选了Druid因为监控页面可视化管理数据库连接答辩时打开Druid监控截图给评委看比空口解释连接池配置有说服力。2.2 缓存与鉴权机制缓存用了Redis但没敢滥用到“万物皆可缓存”的程度只做了三个场景首页热销商品的缓存、商品详情页的缓存、以及登录token的黑名单控制。为什么要缓存首页热销商品因为农产品价格波动大但热销榜单变化并不频繁5分钟内从缓存读取、后台变更时主动删除缓存而不是等过期能明显缓解MySQL压力又不会出现数据太旧的问题。鉴权这块没有引入Spring Security那套重武器用了JWT 拦截器的方式实现。原因很现实毕设项目需要你在答辩时快速说清楚“这个token是怎么生成的、怎么验证的、如何续期”Spring Security的过滤器链对初学者来说是个黑盒讲了容易虚不讲又显得没掌握。JWT的自定义工具类加拦截器代码一目了然而且核心逻辑都是自己写的评委追问起来也不会卡壳。不过这里必须说一个Java Web的老坑跨域问题。前端页面和后端接口分开部署时请求会被同源策略挡住我做了全局CorsConfig放行所有来源和方法并设置了允许携带token相关的请求头。2.3 目录分层与模块职责项目没有用微服务那套复杂度就是经典的单体分层但包结构必须清晰这也是评分点之一。我最终采用的层级是Controller - Service - Mapper - Entity另加config、common、utils、dto、vo、interceptor几个辅助包。实际感受下来这种分层的最大好处不是代码量少而是——你面试或答辩时能非常自然地讲出“请求从Controller到Service再到Mapper的流转过程”。这是基本功比花里胡哨的技术更能体现工程素养。工程结构大致如下com.shandong.agri ├── controller // 控制层买家、卖家、管理端分开 ├── service // 业务层接口实现分离 ├── mapper // 数据访问层 ├── entity // 数据库实体 ├── dto // 前端传参对象 ├── vo // 返回视图对象 ├── config // 配置类含跨域、拦截器注册 ├── interceptor // JWT验证拦截器 ├── common // 统一返回结果、枚举、常量 ├── utils // JWT工具、上传工具、日期工具 └── exception // 全局异常处理Controller中尽量只做参数接收和结果封装Service里放业务规则比如库存扣减和订单状态流转这部分不能塞在Controller里。我也见过很多同学把几百行业务代码写在Controller里当时觉得简单后来加需求的时候重构到怀疑人生。3. 数据库设计农产品交易的核心建模3.1 核心表结构与设计取舍数据库是一个电商系统的灵魂也是答辩时被追问最多的部分。我最终设计了7张核心业务表3张基础表这里不做全字段罗列说几个关键点和设计思路。user用户表字段包含角色标识普通用户/卖家/管理员、昵称、手机号、密码BCrypt哈希、头像、余额。角色控制不在表上做复杂设计直接一个role字段区分配合拦截器判断接口权限。product商品表除了常规的名称、价格、主图、详情轮播图额外加了originplace字段用于产地、season标签字段用于季节、unit字段斤/盒/件、stock字段、sales字段、status上架/下架/待审核、seller_id。这一张表就承接了“特色农产品”的差异化需求。category分类表两级分类设计比如“水果”下面挂“苹果”“梨”“樱桃”“蔬菜”下面挂“大葱”“大蒜”通过parent_id实现级别。cart购物车表以user_id关联用户商品价格字段冗余存储避免查询时还要join商品表。orders订单表包含order_no订单号、user_id、seller_id冗余便于卖家查询、total_amount、status0待支付 1已支付 2已发货 3已完成 4已取消 5退款中、address快照字段、remark、pay_time、ship_time。order_item订单明细表快照商品名、图片、单价、数量、产地信息为什么做快照因为商品信息后续可能被修改、删除但用户的订单凭证必须保留购买当时的事实这是电商订单设计的标准做法。banner轮播图首页运营位后台可配置跳转链接。dict字典表管理季节/产地/规格等枚举性的信息减少硬编码。3.2 状态机订单状态与冗余字段的设计订单状态流转是电商项目里最容易讲出深度的地方。我设计的orders表用status字段存当前状态同时用多个时间字段记录流转节点——pay_time、ship_time、finish_time、cancel_time这些字段能直接支撑管理后台的数据统计。状态流转规则如下买家创建订单后状态为“待支付”此时生成唯一订单号采用时间戳随机数拼接。买家点击“支付”沙箱模拟成功后状态切为“已支付”记录支付时间。卖家看到已支付订单点击“发货”填写物流单号可以随便填字符串模拟状态切为“已发货”。买家收到货后“确认收货”状态切为“已完成”。超时未支付的订单在“待支付”状态下由定时任务扫描并自动取消释放库存。这里有两个设计细节值得讲一是库存锁定语义我在下单接口里不是直接在菜单扣减库存而是增加了一个缓存扣减的操作Redis中扣减支付成功后SQL再扣减一次如果超时取消则回补。这个细节可以在答辩时展示“你能意识到超卖风险”。二是卖家id冗余到订单表常规做法是通过商品表关联卖家进而查询卖家的订单但订单表冗余一个seller_id后卖家中心列表查询直接从订单表过滤性能好实现也简单。订单足够多时这个冗余字段的收益非常明显。3.3 针对农产品的特殊字段落库农产品和普通标准商品最大的区别就是属性不稳定我在product表里设计了几个特色字段专门支撑差异化功能originplace产地支撑按“山东各地市产品名”筛选比如“烟台苹果”。season季节标签春季、夏季、秋季、冬季后台发布商品时勾选前端首页有“应季水果”快捷入口。unit_desc规格描述因为不是精密电子商品描述型字段比死板的规格参数表更灵活例如“约2斤装上下浮动0.1斤”。batch_no采摘批次同一商品在不同批次的价格可能不同虽然毕设没做到多批次价格切换但这个字段为后续扩展预留了坑位。这几张表建好后用Navicat的模型功能直接生成ER图粘贴到论文里再配一段文字描述“为什么要冗余、为什么要快照”这部分论文内容基本就稳了。4. 核心功能实现从接口设计到关键代码4.1 统一返回结果与全局异常处理写接口的第一步不是写业务而是先把统一返回结构定下来。我定义了一个ApiResult类包含code、msg、data三个字段所有Controller的返回类型都是它。前端拿到后根据code判断是否成功200表示成功500表示业务失败401表示token失效。public class ApiResultT { private Integer code; private String msg; private T data; public static T ApiResultT success(T data) { ApiResultT result new ApiResult(); result.setCode(200); result.setMsg(操作成功); result.setData(data); return result; } public static T ApiResultT error(Integer code, String msg) { ApiResultT result new ApiResult(); result.setCode(code); result.setMsg(msg); return result; } }全局异常处理用RestControllerAdvice捕获业务异常、参数校验异常和未知异常保证错误信息以统一格式返回给前端而不是直接把异常堆栈抛给接口。这里有个实操小技巧自定义一个BizException业务异常service层里判断库存不足、重复下单、token失效时直接throw然后由全局异常处理器转成标准返回Controller里的代码能干净一大半。4.2 JWT登录鉴权的完整实现登录鉴权我用的方案是用户输入手机号和密码比对数据库中的BCrypt哈希值成功后生成一个有效期2小时的JWT前端把token存在本地请求时放在Header中的Authentication字段里。public class JwtUtils { private static final String SECRET shandong-agri-project-secret-key; private static final long EXPIRE_TIME 2 * 60 * 60 * 1000; public static String createToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }拦截器里做的事情很简单排除登录、注册、首页商品列表等白名单接口其余所有请求都校验Authorization头里的token解析成功则把userId和role放进request attribute中后续Controller直接从request里取值避免在参数上重复传用户id。管理端接口再额外校验role是否为管理员。这里我踩过一个坑JWT的SECRET不要太短否则启动时会报“弱密钥”异常顺手给他换了足够长的字符串。另外token是在拦截器里校验消息签名千万别把secret硬编码到前端代码里。4.3 商品发布与多条件检索商品发布接口是卖家端的核心Controller接收一个ProductDTO里面包含商品基本信息、产地、季节标签、图片URL图片上传走OSS或者本地存储均可。发布时默认status为0待审核管理员后台审核通过后变成1上架状态。这个审核流程是农产品平台区别于普通商城的重要设计防止不合规商品直接流入前台。检索这块用了MyBatis-Plus的LambdaQueryWrapper动态拼接条件。前端传什么条件就拼什么条件不传就查全部。public PageProduct searchProducts(ProductQueryDTO query) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); // 商品名模糊搜索 if (StringUtils.hasText(query.getKeyword())) { wrapper.like(Product::getName, query.getKeyword()); } // 分类筛选 if (query.getCategoryId() ! null) { wrapper.eq(Product::getCategoryId, query.getCategoryId()); } // 产地筛选 if (StringUtils.hasText(query.getOrigin())) { wrapper.eq(Product::getOriginplace, query.getOrigin()); } // 季节标签 if (StringUtils.hasText(query.getSeason())) { wrapper.eq(Product::getSeason, query.getSeason()); } // 只查询已上架商品 wrapper.eq(Product::getStatus, 1); // 按销量倒序 wrapper.orderByDesc(Product::getSales); return productMapper.selectPage(new Page(query.getPageNum(), query.getPageSize()), wrapper); }当时在筛选条件这一块有个改进很小的点但效果明显产地和季节用下拉选项框而不是让用户手动输入。因为数据库里的产地值都是维护好的手动输入会出现“烟台”和“烟台市”这种语义重复不好过滤。后来在后台做了一个产地下拉的数据来源接口这个问题就解决了。4.4 购物车与下单的事务控制购物车加购时我会在service层校验商品状态为已上架、库存是否充足、购物车是否已有同款商品如果有同款就直接加数量避免重复行数据。下单是电商项目里事务控制的重点。我的下单接口在一个事务里完成以下动作从购物车中取出用户勾选的商品数据重新查询商品当前库存并校验是否充足生成主订单orders保存地址快照、商品快照、总金额批量插入订单明细order_item扣减库存同时增加销量清空已下单的购物车记录。重点说一下第2步的“重新查询库存”前端传来的商品数量和金额不可信防超卖必须以后端查询为准。事务加上后同一时间并发下单时MySQL的行锁会保证扣减不超卖这是电商后端最基本的底线思维。Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateDTO dto) { // 1. 从购物车查商品列表 ListCart cartList cartService.getCheckedItems(dto.getUserId()); if (cartList.isEmpty()) { throw new BizException(请先勾选要结算的商品); } // 2. 遍历校验库存并计算总金额 BigDecimal totalAmount BigDecimal.ZERO; for (Cart cart : cartList) { Product product productMapper.selectById(cart.getProductId()); if (product null || product.getStatus() ! 1) { throw new BizException(商品 cart.getProductName() 已下架); } if (product.getStock() cart.getQuantity()) { throw new BizException(商品 cart.getProductName() 库存不足); } totalAmount totalAmount.add(product.getPrice().multiply(BigDecimal.valueOf(cart.getQuantity()))); } // 3. 生成订单号并插入订单 String orderNo generateOrderNo(); // ... 插入主订单和明细 // 4. 扣减库存 // 5. 清空购物车 return orderVO; }4.5 订单超时自动取消与库存回补这个功能是电商平台的加分项我用了Spring的Scheduled定时任务来实现。每30秒扫描一次所有“待支付”状态的订单凡是创建时间超过15分钟的都自动取消同时回补库存。Scheduled(fixedDelay 30000) public void cancelTimeoutOrders() { LambdaQueryWrapperOrder wrapper new LambdaQueryWrapper(); wrapper.eq(Order::getStatus, 0) .lt(Order::getCreateTime, new Date(System.currentTimeMillis() - 15 * 60 * 1000)); ListOrder timeoutOrders orderMapper.selectList(wrapper); for (Order order : timeoutOrders) { // 锁定订单并更新状态为已取消 order.setStatus(4); order.setCancelTime(new Date()); orderMapper.updateById(order); // 回补库存 ListOrderItem items orderItemMapper.selectList( new LambdaQueryWrapperOrderItem().eq(OrderItem::getOrderId, order.getId())); for (OrderItem item : items) { Product product productMapper.selectById(item.getProductId()); if (product ! null) { product.setStock(product.getStock() item.getQuantity()); productMapper.updateById(product); } } } }这里有个小坑需要提醒定时任务在集群模式下会重复执行但毕设是单机部署问题不大。在启动类上记得加EnableScheduling注解不加的话这个任务永远不会跑。我最初调了一下午没反应检查半天才发现是漏了这个注解。4.6 管理后台数据统计管理后台如果只做CRUD答辩时说服力不够。我用一个首页统计接口返回了四组数据用户总数、商品总数、订单总数、近7日成交金额再用一个折线图接口返回最近30天每日订单数。实现逻辑都不复杂一个SQL统计就能搞定// 近7日成交金额统计 public ListMapString, Object last7DaysOrderAmount() { String today LocalDate.now().toString(); String sevenDaysAgo LocalDate.now().minusDays(6).toString(); return orderMapper.selectDailyAmount(sevenDaysAgo, today); }Mapper里的SQL大致是SELECT DATE(create_time) AS day, SUM(total_amount) AS amount FROM orders WHERE status IN (1, 2, 3) AND create_time BETWEEN #{start} AND #{end} GROUP BY DATE(create_time)这样一来管理端的首页就不是一张白纸而是有真实数据的运营仪表盘无论是论文截图还是演示时都能加分。前端如果用了ECharts直接把后端返回的Map数组映射到xAxis和series即可。5. 前端展示与交互的关键逻辑5.1 页面结构与路由设计前端我用了Vue2 Element UI组件库选型上求稳因为毕设对技术新潮度要求不高管理端和买家端分别建了独立路由。买家端主要页面包括首页、商品分类页、商品详情页、购物车页、订单确认页、个人中心、我的订单列表。管理端就是一套后台布局嵌套菜单栏包含商品管理、分类管理、用户管理和订单管理、数据统计。这里面有两点值得说一下。第一是详情页的“产地直供”展示逻辑。商品接口返回的vo对象里除了商品基础信息还带一个商家店铺名称和一个产地徽标详情页显眼的位置展示“产地烟台市-栖霞市”再配上一条描述“当季现摘产地直发”。如果是自己从零搭页面这个信息需要后端的productVO里冗余返回前端才能直接渲染。第二是购物车勾选状态的设计。购物车页面的勾选状态保存在前端的data模型里没有每次点击都同步后端。只有点“去结算”时把勾选的cartId列表传给后端。这种“前端临时状态后端最终确认”的模式比动不动就调接口要轻盈得多也是实际开发中的常见做法。5.2 图片上传与回显的本地方案毕设项目图片存储一般不会真的接OSS我在后端写了一个通用的文件上传接口接收MultipartFile存储到本地磁盘的upload目录然后返回可访问的URL。这里有一个非常关键的配置把upload目录映射成静态资源路径否则图片在浏览器里无法直接打开。Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath System.getProperty(user.dir) /upload/; registry.addResourceHandler(/upload/**) .addResourceHandler(/upload/**) .addResourceHandlers(ResourceHandlerRegistry::addResourceHandler); }代码写得很简单核心是告诉SpringURL前缀是/upload/的请求去系统当前目录的upload文件夹下找文件。开发时直接用本机路径没问题但部署到Linux服务器时user.dir的路径要提前确认否则上传的文件会出现在你不知道的目录里。5.3 支付模块的模拟处理很多毕设同学一提到支付就想接支付宝沙箱或微信支付说实话这个成本大而且容易卡在资质认证上。我的做法是做一个“模拟支付弹窗”订单确认页点击“提交订单”后端生成订单后跳到支付确认页页面上展示应付金额点击“模拟支付成功”按钮直接调用后端的支付回调接口把订单状态从待支付改成已支付。这个设计在答辩时怎么说可以讲核心的支付状态流转、支付结果回调、订单状态同步逻辑都做好了支付网关因为资质原因用本地mock代替真实环境替换为一个HTTP调用即可。很多评委是认可这种思路的因为毕设考察的是业务闭环和代码能力而不是你真的有个支付牌照。后端支付接口的核心逻辑很短public void mockPay(Long orderId) { Order order orderMapper.selectById(orderId); if (order null || order.getStatus() ! 0) { throw new BizException(订单状态异常无法支付); } order.setStatus(1); order.setPayTime(new Date()); orderMapper.updateById(order); }6. 常见问题与排查技巧实录6.1 启动类与Mapper扫描的坑我遇到过的最诡异的问题是项目能启动但创建的定时任务不触发后台任务全部没有反应。这个的排查耗时较长最后发现是启动类忘了加EnableScheduling。类似地MyBatis-Plus的Mapper接口如果没有加MapperScan注解Spring容器里就不会有Mapper代理Bean所有数据访问接口注入都会失败。建议在启动类上写清楚SpringBootApplication MapperScan(com.shandong.agri.mapper) EnableScheduling public class AgriApplication { public static void main(String[] args) { SpringApplication.run(AgriApplication.class, args); } }6.2 跨域与请求头token丢失前端联调时经常出现两种问题一种是浏览器跨域拦截导致接口无响应另一种是后端说收到不到token。第一种用全局CorsConfig解决第二种要检查前端请求拦截器里是否把token放到了请求头。Vue里Axios的统一封装写法是service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] token; } return config; });记得前端的token键名和后端拦截器读取的Header名保持一致我之前一个项目里前端传的是“Authorization”后端读的是“token”排查到差点怀疑人生。6.3 接口报错但控制台无SQL日志排查这类问题的第一件事就是打开MyBatis的SQL日志输出。在application.yml里配置mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样跑接口时控制台会打印完整SQL语句和参数能非常直观地发现条件拼接错误、字段名不匹配的问题。我用这个日志顺手发现了一个因为Java Boolean字段和数据库tinyint类型映射不准导致的查询条件失效问题。6.4 数据库时区问题导致的统计错误MySQL 8.0连接时如果没指定serverTimezoneJSON里返回的时间会差8个小时管理后台统计的日期会整体错位。解决方案比较简单在JDBC连接串里加上参数jdbc:mysql://localhost:3306/agri_platform?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8除了这个实体类里的日期字段建议统一用java.util.Date而不是LocalDateTime因为和前端交互时LocalDateTime序列化格式不如Date好控尤其涉及到Vue的日期组件展示时Date类型可以直接转时间戳。7. 项目演示与答辩准备的三个要点7.1 演示数据要提前“养”很多同学项目写完了、代码没问题但一打开首页空荡荡的观感很差。演示前一定要手动往数据库里灌一批高质量的种子数据10个分类、30件商品覆盖不同产地和季节、5个买家账号、2个卖家账号、1个管理员账号。商品图片用网上找的美观实拍图描述要真实细致比如“栖霞红富士单个约250g脆甜多汁现摘现发”。数据养得越像真的演示效果越好评委第一印象就越稳。7.2 演示路径要有“剧本”给评委演示时不能想到哪点哪设计一条业务闭环路径买家注册登录→首页按产地浏览商品→加入购物车→下单支付→卖家发货→买家确认收货→管理端查看统计。一路演示下来把用户端、卖家端、管理端都覆盖到每个环节停2-3秒提一句自己做过什么优化比如“这里我做了库存锁定的并发处理”“这里订单超时会自动取消并回补库存”这些高光点不提前说评委很容易忽略。7.3 源码讲解时的表达策略答辩时被要求讲源码的话建议按这个顺序讲启动类配置 - 拦截器鉴权流程 - 商品检索的动态SQL - 下单事务方法 - 定时任务。每一段控制在1分钟以内讲清楚“输入是什么、做了什么、输出怎么处理”。尤其下单事务方法用Transactional注解挂在Service方法上要主动说明“防止库存超卖”几乎每个评委都会对这个点有反应。源码讲解时不要埋头读代码边讲边切到流程控制在10分钟以内收尾。我个人做完这个项目最大的感受是电商类毕设的决定性差异不在技术多新而在业务闭环是否完整、细节是否经得起追问。把农产品特有的产地、季节、批次库存、订单状态机讲透比堆砌一堆中间件有用得多。如果后续想扩展可以考虑接入真实的物流接口、增加拼团/预售功能、或者把管理端做成前后端分离部署这套项目的基座都能撑得住。