
做毕设或者接外包的朋友应该对这题不陌生springboot乡村振兴农产品销售系统一个听起来大而全的管理类项目实际上手要踩的坑却不少。源码28458这个编号我见过好几个版本结构大同小异核心都是围绕农产品信息管理、商品交易、订单流转这一套东西。我帮人排查过几次这类项目的问题也自己基于这个骨架改过几版今天把整个系统的搭建思路、表结构、关键代码和运行时的常见坑全盘捋一遍给准备开干或者正在改这个项目的朋友一份可以直接参考的记录。讲解的顺序我纠结过一下是先讲代码还是先讲表结构我的建议是先从数据库入手因为这个系统所有业务逻辑都是围绕着商品、订单、用户这三者的关系展开的表结构搞清楚了代码看下来自然就顺了。1. 项目需求拆解不只是增删改查是三个角色的协同系统1.1 乡村振兴农产品销售系统的真实业务边界很多人拿到这类项目第一反应是不就是一个商城吗真动手才发现事情没那么简单。这个系统里至少要同时容纳三类使用者他们的诉求完全不同普通消费者也就是C端买家需要能浏览商品、按分类或关键词搜索、查看农产品详情、加购物车、下单、查看订单状态。这里容易忽略收货地址管理很多初始版本不做这个功能但真实交易场景必须有地址选择。农户/商家提供农产品的卖家核心诉求是商品管理——发布新农产品、编辑价格和库存、上架下架、处理订单发货。但注意很多毕设版本的商家功能极弱只是管理员在后台代录商品这种设计虽然省事却不太贴合乡村振兴实际业务逻辑答辩的时候容易被问住。系统管理员平台运营方用户管理、商品审核、分类维护、订单监管、公告发布。部分做得精细的版本还会有简单的销售数据统计比如按月份统计订单金额走势、按分类统计销量占比。这三类角色对应到代码落地就是一个基于Spring Boot的后端服务配合Vue/Thymeleaf等前端方案提供登录鉴权、权限拦截、商品模块、订单模块、支付多数是模拟支付、后台管理这几大块。1.2 业务链路核心一个订单的生命周期我把整个系统最核心的业务链路画在脑子里大概是这样的农户发布商品 → 管理员审核通过 → 商品上架展示 → 消费者浏览搜索 → 加入购物车 → 提交订单选地址 → 模拟支付 → 订单状态变为待发货 → 农户发货 → 待收货 → 消费者确认收货 → 订单完成这条链路里最需要注意的是订单状态的流转因为它是整个项目里事务最多、最容易出bug的地方。后面我会专门列出订单表设计时的状态机字段这里先记住一个原则订单状态不要用字符串随意写要用数字枚举且每一步状态变更都必须有对应的方法和记录。1.3 容易忽略但答辩加分的小模块这个标题里带乡村振兴四个字做的时候不能只套一个普通商城壳子。我见过做得好的版本会在首页加一个产地直供展示区商品表里带上产地字段比如某个镇、某个村加一个滞销农产品帮扶专区甚至有的版本做了简单的溯源信息展示产地照片、农户简介、种植过程介绍。这些改动技术难度不大但能在展示项目时明显提升整体立意建议源码拿到手后在这些维度做二次开发。2. 技术选型与项目初始化Spring Boot版本和依赖是最容易炸的地方2.1 框架选型对比为什么不选SSH而选Spring Boot现在做这套系统Spring Boot几乎是共识但为什么这个点上很多人答不上来。和早期的Spring SpringMVC MyBatis的SSM架构比Spring Boot最大的贡献是自动配置它把Web容器Tomcat、数据源、MyBatis、JSON序列化这些繁琐的XML配置全部用约定取代了。你引入一个spring-boot-starter-web依赖它自动帮你配好DispatcherServlet、内嵌Tomcat、消息转换器这在SSH时代是不可想象的。和Spring Cloud那套微服务比农产品销售系统的业务复杂度远没到需要拆服务的程度——单体应用部署简单、调试方便、对服务器资源要求低更适合毕设和小型项目落地。所以技术选型的结论很清晰Spring Boot单体 MyBatis或MyBatis-Plus MySQL / Redis 前端Vue或模板引擎。2.2 版本选择的血泪教训我处理过好几个启动失败的案例十有八九是版本兼容问题。这里直接把我测试可行的版本组合列出来供参考组件推荐版本说明JDK1.8 或 11部分高版本Spring Boot要求JDK17但配套组件不一定全兼容建议稳妥为主Spring Boot2.7.x不要去追3.x很多老版教程和依赖还不适配MyBatis-Plus3.5.x如果源码用的是原生MyBatis升级到Plus能省掉大量XMLMySQL5.7 或 8.08.0要注意驱动名和时区配置Redis5.x以上不是核心依赖但用了做缓存或会话共享时需要Maven3.6太低版本会拉不下来依赖这里有个隐蔽问题必须说如果你的Spring Boot版本太低比如2.2.x以下配合高版本MySQL驱动会报SSL和时区错误。解决方案是在数据库连接串上加上这几个参数useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue。源码28458这个版本的application.yml里如果没配齐启动连库的时候大概率要折腾一番。2.3 项目目录结构规划一套清晰的分层结构长这样拿到源码后建议先按这个核对一遍com.example.rural ├── config # 配置类拦截器、CORS跨域、Swagger ├── controller # 控制层接收请求、参数校验、返回结果 ├── service # 业务层核心逻辑处理 │ └── impl ├── mapper # 数据访问层继承BaseMapper如果用了Plus ├── entity # 实体类和数据库表一一对应 ├── dto # 数据传输对象用于接收前端提交的复合参数 ├── vo # 视图对象用于返回给前端组合数据 ├── common # 通用类统一返回结果、异常处理、常量 ├── utils # 工具类JWT工具、文件上传工具 └── interceptor # 登录拦截器、管理员权限拦截器很多偷懒的初始版本会把service和mapper混在一起甚至直接在controller里写业务逻辑。这种代码跑起来没有问题但一旦出现下单要同时扣库存生成订单清空购物车这种组合操作事务注解没法切到正确的边界上排查问题会特别痛苦。拿到源码后第一件事就是确认业务的service层有没有独立出来。3. 数据库表结构设计商品、订单、用户这三张表决定系统质量3.1 核心数据表与关键字段解析农产品销售系统的表基本在8~12张之间最核心的我挑出来逐一说明设计意图用户表user字段名类型说明idbigint主键usernamevarchar(50)登录名唯一passwordvarchar(100)BCrypt加密后的密码nicknamevarchar(50)昵称展示用phonevarchar(20)手机号avatarvarchar(255)头像地址roletinyint角色0用户 1商家 2管理员create_timedatetime注册时间role字段是整个权限控制的核心。我见过有版本用is_admin这种布尔字段结果只能区分管理员和普通用户商家角色塞不进去后面加功能时被迫改表结构。建议拿到源码后先看role字段设计如果不是这种可扩展的数字枚举有条件就重构一下。商品表product字段名类型说明idbigint主键category_idbigint所属分类namevarchar(100)商品名称main_imagevarchar(255)主图地址detail_imagestext详情图多张用逗号或JSON存descriptiontext商品描述originvarchar(100)产地比如XX县XX镇pricedecimal(10,2)价格单位元stockint库存salesint销量statustinyint0下架 1上架 2待审核 3审核拒绝create_timedatetime上架时间商品表里我特意加了origin字段这个就是呼应乡村振兴主题的关键设计——农产品不像工业品产地是消费者非常关心的信息甚至能直接影响购买决策。你做展示的时候列表页把产地标识出来整个项目的业务特色就出来了。订单表orders与订单明细表order_item订单表字段名类型说明idbigint主键order_novarchar(50)订单编号格式建议yyyyMMddHHmmss 随机数user_idbigint下单用户total_amountdecimal(10,2)订单总金额statustinyint0待支付 1待发货 2待收货 3已完成 4已取消receiver_namevarchar(50)收货人姓名receiver_phonevarchar(20)收货人电话receiver_addressvarchar(255)收货人地址remarkvarchar(255)订单备注create_timedatetime下单时间pay_timedatetime支付时间deliver_timedatetime发货时间finish_timedatetime完成时间订单明细表字段名类型说明idbigint主键order_idbigint订单IDproduct_idbigint商品IDproduct_namevarchar(100)商品名称快照product_imagevarchar(255)商品图片快照pricedecimal(10,2)成交单价quantityint购买数量total_pricedecimal(10,2)小计金额订单头和订单明细分离是电商系统的经典设计原因很简单一个订单可以包含多种商品如果都塞进订单表一行字段既冗余又没法做统计。另外注意到product_name和product_image这两个冗余字段没有这叫数据快照因为商品表的信息商家随时可能修改但用户买了那一刻的价格和名称必须固定下来不然订单历史记录就会乱套。3.2 订单状态机的实现约束订单状态我建议在Java里定义一个枚举类把每个状态和它的下一步动作绑定起来public enum OrderStatusEnum { WAIT_PAY(0, 待支付), WAIT_DELIVER(1, 待发货), WAIT_RECEIVE(2, 待收货), FINISHED(3, 已完成), CANCELLED(4, 已取消); private final Integer code; private final String desc; OrderStatusEnum(Integer code, String desc) { this.code code; this.desc desc; } public static String getDescByCode(Integer code) { for (OrderStatusEnum status : OrderStatusEnum.values()) { if (status.getCode().equals(code)) { return status.getDesc(); } } return 未知状态; } }为什么要做这个枚举因为它本质上是给订单状态的可视化做统一映射避免在Controller和前端页面上散落着if (status 2)这种魔法数字。改状态的时候只用枚举的code前端用后端返回的desc做展示整个链路清晰很多。3.3 表设计的两个常见坑第一个坑是购物车表要不要建。有人觉得购物车是临时的放Redis里就行不用建表。但农产品销售系统的购物车功能如果只存Redis用户换个设备购物车就丢了体验很差。建议建一张cart表字段含user_id、product_id、quantity、checked是否选中结算、create_time以user_id和product_id做唯一索引。第二个坑是分类表不要写死。我见过有初始版本把商品分类直接在代码里定义成一个常量类这样做一旦想加一个有机蔬菜分类就要改代码重新部署。正确的做法是建category表id、name、sort、create_time后台提供分类管理功能前端用遍历动态渲染。农产品分类变化虽然不频繁但有了表结构扩展起来零成本。4. 核心代码实现JWT鉴权、商品查询、下单事务、文件上传4.1 JWT登录鉴权的完整实现思路现在很多人都知道登录要用JWT但执行起来容易只做一半——只签发token、校验token却忽略了权限拦截和用户信息传递。我先说JWT本身的核心用法。引入依赖基于JJWT库dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependencyJWT工具类核心方法Component public class JwtUtils { // 密钥实际项目中应从配置文件读取不要硬编码 Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; // 生成token public String generateToken(Long userId, String username, Integer role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } // 解析token public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }光有这个工具类不够还要配上拦截器、统一放行白名单、ThreadLocal保存当前用户。具体做法是加一个LoginInterceptor继承HandlerInterceptor接口public class LoginInterceptor implements HandlerInterceptor { Autowired private JwtUtils jwtUtils; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 预检请求直接放行 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(token); if (StringUtils.isEmpty(token)) { throw new BusinessException(401, 未登录); } try { Claims claims jwtUtils.parseToken(token); UserContext.set(claims.get(userId, Long.class), claims.get(role, Integer.class)); return true; } catch (Exception e) { throw new BusinessException(401, 登录已过期请重新登录); } } }注册拦截器的时候注意放行路径比如/api/user/login、/api/user/register、/api/product/**商品浏览无需登录、/images/**静态资源都要排除掉。这里有一个容易犯的错误把所有接口都拦截结果商品列表都看不了登录页也拿不到轮播图数据。调的时候建议把放行规则列出来对照宁可多放一个浏览类接口也不要拦截掉必要的。我处理过一个问题前端静态资源被拦截导致页面白屏排查半天发现是拦截器路径配置成/**把/static/**给拦了。4.2 商品分页查询MyBatis-Plus的LambdaQueryWrapper用法商品列表是用户访问量最大的接口通常需要支持分页、按名称模糊搜索、按分类筛选、按价格和销量排序。用MyBatis-Plus的话这套查询可以写得很优雅public PageResultProductVO getProductPage(ProductQuery query) { PageProduct page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); // 关键字模糊搜索按商品名或产地搜索 if (StringUtils.hasText(query.getKeyword())) { wrapper.and(w - w.like(Product::getName, query.getKeyword()) .or().like(Product::getOrigin, query.getKeyword())); } // 分类筛选 if (query.getCategoryId() ! null) { wrapper.eq(Product::getCategoryId, query.getCategoryId()); } // 状态只查询上架商品 wrapper.eq(Product::getStatus, 1); // 排序支持 price_asc、price_desc、sales_desc if (price_asc.equals(query.getSort())) { wrapper.orderByAsc(Product::getPrice); } else if (price_desc.equals(query.getSort())) { wrapper.orderByDesc(Product::getPrice); } else { wrapper.orderByDesc(Product::getSales); } PageProduct productPage productMapper.selectPage(page, wrapper); // 转成VO补充分类名称等冗余信息 return PageResult.of(productPage, this::convertToVO); }这里用到了LambdaQueryWrapper它是MyBatis-Plus对比原生MyBatis最爽的改进点不用写XML、不用拼字符串编译期就能发现字段错误。如果你的源码还在用原生MyBatis的XML写法我建议逐步往Plus迁移效率至少提升一倍。4.3 下单功能的事务控制一个必须精通的核心方法下单是整个系统里最能体现业务水平的代码。因为它的操作跨多张表任何一步出错都要整体回滚。看下面的标准写法Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateRequest request) { // 1. 计算总价校验地址 ListCartItem cartItems cartMapper.selectBatchIds(request.getCartItemIds()); if (CollectionUtils.isEmpty(cartItems)) { throw new BusinessException(购物车不能为空); } BigDecimal totalAmount BigDecimal.ZERO; ListOrderItem orderItems new ArrayList(); for (CartItem item : cartItems) { Product product productMapper.selectById(item.getProductId()); if (product null || product.getStatus() ! 1) { throw new BusinessException(商品不存在或已下架); } if (product.getStock() item.getQuantity()) { throw new BusinessException(商品[ product.getName() ]库存不足); } // 2. 构建订单明细快照商品信息 OrderItem orderItem new OrderItem(); orderItem.setProductId(product.getId()); orderItem.setProductName(product.getName()); orderItem.setProductImage(product.getMainImage()); orderItem.setPrice(product.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItem.setTotalPrice(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); totalAmount totalAmount.add(orderItem.getTotalPrice()); orderItems.add(orderItem); // 3. 扣减库存防止超卖的关键 int rows productMapper.deductStock(product.getId(), item.getQuantity()); if (rows 0) { throw new BusinessException(商品[ product.getName() ]库存不足); } } // 4. 生成订单主记录 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(UserContext.getUserId()); order.setTotalAmount(totalAmount); order.setStatus(OrderStatusEnum.WAIT_PAY.getCode()); // 地址信息赋值... orderMapper.insert(order); // 5. 批量插入明细 for (OrderItem item : orderItems) { item.setOrderId(order.getId()); orderItemMapper.insert(item); } // 6. 清空已下单的购物车 cartMapper.deleteBatchIds(request.getCartItemIds()); return convertToOrderVO(order); }几个细节值得展开Transactional(rollbackFor Exception.class)这个参数不能省。默认情况下Spring声明式事务只对RuntimeException回滚如果你在事务方法里抛了一个普通的Exception它不会触发回滚极端情况下会出现订单创建了但库存扣了这类不一致问题。显式指定rollbackFor为Exception.class是最稳妥的。扣库存用的productMapper.deductStock(product.getId(), item.getQuantity())对应一条update语句UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}。这种写法把判断库存足够和扣减库存放在一条SQL里原子完成可以有效防止并发超卖。如果先select查库存再update扣库存两个请求同时读到库存为1各扣1就会出现负数这是秒杀场景最基本的并发控制手段写进简历都是亮点。清空购物车放在最后。如果前面步骤报错事务回滚后购物车还留着用户重新下单不会丢数据。这个顺序看起来小事但实际体验差异很大。4.4 文件上传农产品图片上传的实现细节农产品销售系统里商品图片是刚需核心需求是本地存储、限制大小、防重名。Spring Boot里实现文件上传相当直接PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { throw new BusinessException(文件不能为空); } // 限制文件大小单张图片最大5MB if (file.getSize() 5 * 1024 * 1024) { throw new BusinessException(图片大小不能超过5MB); } // 校验文件类型防止上传恶意脚本 String originalFilename file.getOriginalFilename(); String suffix Objects.requireNonNull(originalFilename) .substring(originalFilename.lastIndexOf(.)); ListString allowedSuffix Arrays.asList(.jpg, .jpeg, .png, .gif, .webp); if (!allowedSuffix.contains(suffix.toLowerCase())) { throw new BusinessException(仅支持jpg/jpeg/png/gif/webp格式); } // 重命名避免文件名冲突 String newFilename UUID.randomUUID().toString().replace(-, ) suffix; String datePath new SimpleDateFormat(yyyyMMdd).format(new Date()); String dirPath uploadPath / datePath; File dir new File(dirPath); if (!dir.exists()) { dir.mkdirs(); } File dest new File(dirPath / newFilename); try { file.transferTo(dest); } catch (IOException e) { throw new BusinessException(文件上传失败); } // 返回浏览器可访问的URL return Result.success(/images/ datePath / newFilename); }这个实现里有三层安全细节类型白名单防止上传可执行文件、重命名避免路径遍历攻击、按日期分目录便于后期运维清理。如果你部署的时候发现上传的图片403或者404先检查配置类里有没有加上静态资源映射——把本地的uploadPath映射到/images/**这个URL前缀否则Tomcat永远不会主动把你磁盘上的目录暴露出去。4.5 管理员的商品审核流程有了商家角色之后很多版本会加商品审核环节避免农户随意上架不合规的产品。管理员端有一个待审核列表点通过就执行PutMapping(/admin/product/audit) public ResultVoid audit(RequestBody AuditRequest request) { Product product productMapper.selectById(request.getProductId()); if (product null) { throw new BusinessException(商品不存在); } // 审核通过或驳回 if (request.getPass()) { product.setStatus(1); } else { product.setStatus(3); product.setAuditRemark(request.getRemark()); } productMapper.updateById(product); return Result.success(); }这里有个好习惯驳回的时候加上原因audit_remark字段商家端能看到审核拒绝的理由知道自己该怎么改。这个小设计在答辩时提出来很有产品思维的加分效果。5. 系统部署与运行实践从源码到能跑通的全流程记录5.1 环境准备清单先把基础环境列出来严格按照下面的版本来能省去大量无意义的报错排查JDK 1.8务必确认JAVA_HOME配置正确Maven 3.6配置阿里云镜像加速依赖下载MySQL 5.7或8.0准备一个utf8mb4编码的数据库Redis 5如果项目用了session共享或缓存必须启动IDE推荐IDEA导入Maven项目后先执行mvn clean compile验证依赖源码中默认的application.yml里配置文件有一个点必须核对数据库名称。源码28458这个版本建表脚本一般在sql目录下文件名类似rural_sales.sql。导入时用source命令或者Navicat运行SQL脚本然后修改配置文件中的url、username、password注意账号要有足够的权限。5.2 启动过程常见报错与解决办法错误1端口被占用java.net.BindException: Address already in use: bind解决方案很简单找到占用8080的进程杀掉或者改application.yml里的server.port。改端口有个连锁影响——前端项目里axios的baseURL可能写死了端口要同步改。错误2数据库连接超时Cannot create PoolableConnectionFactory (Communications link failure)先检查MySQL有没有启动再检查账号密码最后检查连接串的时区参数。这里有个经验判断如果是全新环境八成是驱动和MySQL版本不匹配。MySQL 8.0必须用com.mysql.cj.jdbc.Driver老版本驱动连8.0会抛ClassNotFoundException或者认证插件错误。错误3前端请求跨域这个几乎必现。前端跑在8081后端在8080属性完全不同源浏览器会拦截。解决方案在config目录下加一个CorsConfigConfiguration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }有的版本喜欢用CrossOrigin注解加到Controller上这种写法也能用但跨域配置分散到几十个Controller上维护成本高遇到特殊情况还容易漏不如集中到配置类里。5.3 部署到Linux服务器的实操记录毕设项目往往要部署到云服务器上演示我把完整流程压缩成可执行的步骤本机执行mvn package -DskipTests生成jar包target目录下名类似rural-sales-0.0.1-SNAPSHOT.jar。用scp或宝塔文件管理器把jar包传到服务器放在/opt/rural目录。服务器需要安装JDK和MySQL并用mysql命令导入建表脚本mysql -u root -p rural_sales rural_sales.sql用nohup启动nohup java -jar rural-sales-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod app.log 21 查看日志确认启动成功tail -f /opt/rural/app.log云服务器安全组规则里放行端口默认8080或你改的端口。这一步经常被忘本地访问正常服务器IP端口却不通十有八九是安全组或防火墙没放行。部署完成后有几个基本验证动作访问登录接口能签发token吗商品列表有数据吗图片能正常加载吗如果图片加载不出来大概率是application.yml里上传路径配置成了本机绝对路径需要改成服务器的真实路径并在配置类中把对应目录映射到虚拟路径下。6. 二次开发建议手头源码到手后优先改这五个地方很多朋友拿到这套源码不是为了直接用而是为了改造升级。基于我对这个系统的了解建议优先从以下几个角度切入第一引入Redis缓存首页热点数据。首页的商品列表和轮播图属于读多写少的场景每次查询都打MySQL在高并发下扛不住。用Redis做缓存过期时间设置5到10分钟能够显著提升接口响应速度。加依赖、改代码的工作量不大但能在文档中体现你懂缓存设计的思路。第二补一个简单的订单统计接口。管理员后台的Dashboard目前很多版本只有用户数、商品数、订单数这几个硬编码数值你可以加上按月查询订单金额走势的功能。用一条简单的SQL就能搞定SELECT DATE_FORMAT(create_time, %Y-%m) AS month, SUM(total_amount) AS amount FROM orders WHERE status ! 4 GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY month DESC配合前端ECharts画个折线图项目整体观感立刻不一样。第三模拟支付改成更接近真实的流程。现有版本多数是在提交订单后直接把状态改成待发货相当于自动付款了。你可以增加一个确认支付的二次操作前端弹窗模拟扫码后端做一次金额校验虽然还是模拟但业务流程更完整。第四加入导出功能。比如订单列表导出Excel可以用EasyExcel库实现。这个功能在实际应用中使用频率极高面试时也常被问值得花时间实现一遍。第五权限拦截这块值得补一版更细的。现有的做法通常是登录拦截 管理员角色判断两层如果你时间充裕可以参考Spring Security或Sa-Token把接口级别的权限注解做出来这样后端接口的权限控制会更严谨。不过要提醒一句集成Spring Security学习成本不低原本不熟悉的话不建议答辩前临时抱佛脚容易越改越乱。最后再分享一个我在排查这套源码时发现的高频问题很多人在配置图片虚拟路径时写错参数导致商品图全挂。解决方法其实就一段配置类核心是把磁盘物理路径映射到URL访问路径上这里不展开贴代码——因为每台机器路径不同重点是确认uploadPath这个配置项和磁盘真实路径保持一致。这套springboot农产品销售系统整体来说是个相当标准的Java Web项目边界清晰、业务典型非常适合拿来做Spring Boot、MyBatis-Plus、JWT这些技术的练手载体。真正把它吃透的人不止能跑通源码还能从一张表的设计、一个事务方法里看出来项目的基本功。希望这份记录能帮你省下来回踩坑的时间把精力花在真正有增量的功能上。