资讯动态

SpringBoot农贸市场交易系统实战:从需求拆解到部署与答辩

发布时间:2026/10/17 12:37:35 来源:尧图企业网站定制
每年到这个季节后台私信里问得最多的题目之一就是“基于SpringBoot的农贸市场管理系统”和“农产品在线交易平台”。说实话这两个说法经常出现在同一条需求描述里乍一看像两个题目其实内核高度重合——围绕农贸/生鲜场景做一个B/S架构的在线交易系统消费者能下单买菜摊主能管商品和订单平台方能看到全量经营数据。这个方向确实值得选业务场景足够真实技术栈定位清晰从SSM到SpringBoot再到微服务都能自然衔接不管拿来做毕业设计、课程设计还是求职作品集都有得讲。这套系统我前后带过好几个同学落地自己也重构过两版。今天不写那种“开题报告体”的文章直接把从需求拆解、数据库设计、核心功能实现到并发库存、部署上线、答辩包装的完整实操经验摊开来讲。如果你正准备动手做这个题目或者已经在写代码的路上卡住了这篇文章能帮你少走不少弯路。1. 项目需求拆解与整体技术选型1.1 农贸市场交易场景的真实痛点很多同学拿到题目就开始建表写CRUD这是最大的误区。先想清楚一个问题农贸市场的交易和普通电商到底差在哪普通电商卖的是标准化商品比如手机、衣服规格固定、价格稳定、物流周期长。生鲜农产品完全不是这样蔬菜水果按斤卖不同摊位的品质和价格都不一样运输和存储讲究时效消费者更看重“今天下单今天能送到”或者“就近自提”。另外农贸市场本身的经营模式是“多个独立摊位一个市场管理方”平台不能只服务一个卖家而是要同时服务几十上百个摊主每个摊主有自己的商品池和订单。这意味着系统要拆成三类角色消费者端注册登录、浏览摊位和商品、加购物车、下单支付、查看订单状态、确认收货和评价。商家端摊主登录后管理自己的商品、上下架、改库存、处理订单发货/退款、查看当日营业额。平台管理端审核摊位入驻、管理商品类目、处理用户反馈、查看全平台订单流水、看销售统计图表。把这三个角色理清楚整个系统的功能边界就出来了。后面的数据库设计、接口设计、权限设计全部围绕这三条线展开。1.2 技术选型为什么是SpringBoot B/S选型这个问题答案是明确的SpringBoot MyBatis-Plus MySQL Redis Spring Security(JWT) 这套组合是目前最稳妥的方案。为什么SpringBoot因为它解决了传统SSM项目配置繁琐的问题。做毕设或者个人项目精力应该花在业务逻辑上而不是花在配置XML和依赖冲突上。SpringBoot的自动装配让项目几分钟就能跑起来而且社区资料极多遇到任何报错都能搜到解决方案。对需要快速迭代的项目来说这就是最大的优势。B/S架构这个点也要能讲清楚。所谓B/S就是Browser/Server用户通过浏览器访问系统不需要安装客户端。对比C/S架构比如老式的桌面收银软件B/S的好处是部署一套服务所有人通过网址就能用商家端和管理端也一样浏览器里登录就行。这和农贸市场的场景非常匹配——摊主不需要懂技术给他一个网址和账号就能用。辅助选型上MyBatis-Plus相比原生MyBatis多了通用Mapper和分页插件增删改查少写大量重复代码。Redis用来做缓存、验证码存储、购物车临时数据以及后面要讲的锁库存和超时关单。MySQL关系型数据存储稳定成熟。Spring Security JWT做用户认证和接口权限控制比Session方案更适合前后端分离。这套技术栈尤其适合“题目里写了SpringBoot”的硬性要求。注意一点如果题目里要求SSM那就在SpringBoot基础上换成Spring MVC MyBatis的写法逻辑是一样的。1.3 功能模块拆分与项目结构规划拿到需求后不要急着写代码先把模块拆出来。我习惯用一个简单的表格把功能清单列清楚之后再决定先做哪块、后做哪块。模块消费者端商家端管理端账号体系注册/登录/个人信息登录/摊位资料管理员登录商品管理浏览商品/搜索/按摊位筛选商品上架/下架/改价/调库存类目管理/商品违规下架交易链路购物车/下单/支付/订单查询接单/发货/退款处理订单查看/售后仲裁运营管理评价/收藏查看摊位销量用户禁用/摊位审核/数据统计项目结构上推荐用标准的maven结构分包清晰一点controller、service、mapper、entity、dto、vo、config、common。很多同学喜欢把代码全堆在controller里刚开始觉得快后面改需求的时候痛苦不堪。按层分包看起来多花了一点时间但对维护和答辩都是加分项。另外建议前后端分离前端用Vue或React都行。如果前端基础较弱用Vue3 Element Plus ECharts这套组合最省心组件现成、文档全。如果时间紧张也可以直接用Thymeleaf做服务端渲染能减少很多联调工作。我个人更推荐前后端分离因为答辩时面试官通常会对接口设计和跨域处理感兴趣这是亮点来源。2. 核心数据模型与状态设计2.1 三端融一用户表的角色设计第一个关键设计决策是用户表怎么建。最偷懒的误解是建三张表用户表、商家表、管理员表。这样做的后果是登录逻辑要写三遍权限校验要写三遍用户和商家如果要关联比如同一个手机号注册还得中间表。推荐做法是一张用户表通过role字段区分角色。角色取值建议为USER消费者、MERCHANT商家、ADMIN管理员。商家额外需要一份店铺信息所以再建一张merchant_info表用户表只存登录凭证和个人基础信息。用户表的核心字段id主键username / password登录凭证密码加密存储phone手机号用于登录或找回密码role角色枚举status状态正常/禁用balance账户余额如果做成钱包支付会用到create_time注册时间商家信息表iduser_id关联用户表shop_name摊位名shop_type主营类目蔬菜/水果/肉类audit_status入驻审核状态待审核/通过/拒绝description、shop_img摊位描述和头像这样设计的好处是登录接口只需要查一张表拿到用户名、密码和角色然后交给Spring Security做角色校验。后面扩展“一个用户既是消费者又申请了摊位”这种场景也非常自然用户表不动只往商家信息表里加一条关联。2.2 商品、库存与价格精度设计商品表是整个交易系统的核心。农产品有一个特点规格不统一可能是“一斤”可能是“一份”也可能是“一箱”。所以在商品表里一定要有规格字段否则下单时没法算总价。另外价格字段必须用BigDecimal不能用double或float这是电商系统的基本常识。凡是涉及钱浮点数都会出现精度问题比如0.1 0.2不等于0.3。商品表核心字段可以这样设计CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, shop_id BIGINT NOT NULL COMMENT 所属摊位, category_id BIGINT NOT NULL COMMENT 商品类目, name VARCHAR(100) NOT NULL COMMENT 商品名称, spec VARCHAR(50) DEFAULT 1份 COMMENT 规格描述如1斤/500g, price DECIMAL(10,2) NOT NULL COMMENT 单价, stock INT NOT NULL COMMENT 库存数量, image VARCHAR(255) COMMENT 商品图片, status TINYINT DEFAULT 1 COMMENT 上架状态 1上架 0下架, sales INT DEFAULT 0 COMMENT 销量, create_time DATETIME );库存字段要重点思考。生鲜商品的库存和服装库存不一样补货周期短、易损耗很多时候摊主会临时调整。所以库存操作要非常谨慎具体方案在下面“下单扣库存”部分展开。这里先明确一个设计原则库存不能直接变成负数扣库存必须和下单操作放在同一个事务里处理。2.3 订单状态机与超时处理机制订单表是整个系统里状态最复杂的表。如果没有提前设计好状态机写业务逻辑时一定会乱。建议状态字段直接使用整数枚举按以下链路流转待支付状态0已支付/待发货状态1已发货状态2已完成状态3已取消状态4退款中状态5已退款状态6注意几点用户创建订单后默认进入待支付如果一定时间内未支付订单要自动取消并释放库存这就是“超时关单”支付成功后进入待发货商家发货后进入待收货用户确认收货后订单完成。生鲜电商还有一个特点退款比例比普通电商高因为商品容易腐烂、送错。所以退款功能建议在毕设里做成优先完整的一环至少要有“用户发起退款 - 商家同意/拒绝 - 平台介入”的滑动流程。这里我直接用状态机图来描述但因为是纯文本博客用表格表达清楚当前状态触发动作下一状态待支付用户支付待发货待支付超时未支付已取消待发货商家发货待收货待收货用户确认收货已完成待发货/待收货用户发起退款退款中退款中商家同意已退款退款中商家拒绝回到原状态订单表还要存储下单时的商品快照。注意“快照”这个词很关键。用户下单后商家改价、改商品名都不应该影响已经生成的订单。所以订单明细表里要冗余商品名称、单价、规格、图片等字段而不是在下单时只存一个product_id。很多初学者在这里踩坑下单后商家改了价格用户订单金额也跟着变了那就是没有做快照。2.4 JWT鉴权体系的设计思路前后端分离项目里最常用的认证方案是JWTJSON Web Token。它的核心思想是用户登录成功后服务端生成一个签名字符串返回给前端前端后续每次请求都在请求头里携带这个token服务端验证token合法后就知道当前用户是谁。使用JWT有几个明显好处无状态服务端不需要保存Session适合分布式部署。token里可以携带用户ID、角色等基本信息减少数据库查询。跨域环境下处理简单前端把token放在Authorization头里就行。JWT的缺点是一旦签发在过期之前很难主动吊销。所以建议设置较短的过期时间比如2小时同时配合Redis做“用户退出登录即拉黑token”的逻辑。我的实际项目里这样落地public String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 2 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }接口权限控制使用Spring Security的注解方式比如商家端接口加PreAuthorize(hasRole(MERCHANT))管理员接口加PreAuthorize(hasRole(ADMIN))这样角色校验逻辑就收口了不用在每个Controller里写if判断。3. 关键功能实操从登录到下单支付3.1 项目初始化与环境准备环境准备这部分直接列清单照着做就行JDK 8或11稳定优先不要追新Spring Boot 2.x配JDK 11最稳Maven 3.6MySQL 5.7或8.0Redis 6.x基本功能不需要额外插件IDEA 或者 Eclipse建议IDEA在Spring Initializr上生成项目选择Web、MyBatis、MySQL Driver、Redis、Security相关依赖。注意如果选了Spring Security要记得先放行登录接口否则项目一启动所有接口都会被拦截这是新手最容易卡住的地方。pom.xml关键依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency数据库初始化不用手工一条条建表直接用SQL脚本一次性建好再往商品类目和测试数据表里插入一些演示数据。这一步一定不要省因为答辩演示的时候空数据库根本展示不出效果。3.2 用户注册登录与接口鉴权落地登录接口的逻辑很简单但细节容易出错。推荐的处理流程前端传username、password。后端查询用户表校验密码BCrypt匹配。校验通过后生成JWT同时把用户信息存一份到Redis键为login:user:{userId}。返回token和基础用户信息给前端。注册接口需要注意的是密码加密。千万不能用明文存库直接用BCryptPasswordEncoder加密。PostMapping(/register) public Result register(RequestBody RegisterDTO dto) { // 检查用户名是否已存在 User user new User(); user.setUsername(dto.getUsername()); user.setPassword(passwordEncoder.encode(dto.getPassword())); user.setRole(USER); user.setStatus(1); userService.save(user); return Result.success(注册成功); }自定义JWT过滤器核心思路是从请求头获取token如果存在就解析并放入SecurityContext让后面的注解切面能拿到当前用户角色。这个过滤器每人写法略有差异但注意一点必须放行登录、注册、部分商品浏览接口否则前端调登录接口会被拦截。安全配置类里这样配置http.csrf().disable() .authorizeRequests() .antMatchers(/api/auth/login, /api/auth/register, /api/product/**).permitAll() .antMatchers(/api/merchant/**).hasRole(MERCHANT) .antMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated();这里Product开头的浏览接口放行了但购买、下单等接口必须登录。商品浏览是游客也可以看的否则没法吸引用户注册。3.3 商品查询与购物车实现商品模块相对简单但要注意分页和条件筛选。用MyBatis-Plus的分页插件加一个QueryWrapper按商品名称模糊查询、按类目筛选、按价格排序。另外生鲜电商的特殊需求是“按摊位看菜”所以查询条件要能传入shopId前端展示某个摊位下的所有商品。商品列表接口设计GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, String keyword, Long categoryId, Long shopId) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(keyword), Product::getName, keyword) .eq(categoryId ! null, Product::getCategoryId, categoryId) .eq(shopId ! null, Product::getShopId, shopId) .eq(Product::getStatus, 1); return Result.success(productService.page(new Page(page, size), wrapper)); }购物车实现有两种方案数据库存储和Redis存储。推荐用Redis因为购物车高频读写、不需要永久持久化Redis的Hash结构正好适合。键设计为cart:{userId}字段为productId值为数量。这样加减购物车都是O(1)操作性能很好。3.4 下单、支付与库存扣减下单是整个系统最复杂的环节也是面试官最常追问的地方。正常流程分为三步1 创建订单。事务开启查询商品、校验库存、生成主订单和订单明细、扣减库存可预占、订单状态设为待支付。2 支付。支付接口接收订单号调用模拟支付逻辑也可以接第三方沙箱支付成功后修改订单状态为待发货然后返回前端跳转结果。3 超时关单。定时任务扫描超过30分钟未支付的订单把状态改为已取消同时恢复库存。核心代码逻辑如下Transactional(rollbackFor Exception.class) public Order createOrder(Long userId, ListCartItem items) { // 生成订单号时间戳 随机数 String orderNo generateOrderNo(); Order order new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setStatus(0); ListOrderItem orderItems new ArrayList(); for (CartItem item : items) { Product product productMapper.selectById(item.getProductId()); if (product null || product.getStatus() ! 1) { throw new BizException(商品已下架); } if (product.getStock() item.getQuantity()) { throw new BizException(库存不足 product.getName()); } // 扣减库存 product.setStock(product.getStock() - item.getQuantity()); productMapper.updateById(product); // 商品快照 OrderItem orderItem new OrderItem(); orderItem.setProductId(product.getId()); orderItem.setProductName(product.getName()); orderItem.setProductPrice(product.getPrice()); orderItem.setProductImage(product.getImage()); orderItem.setQuantity(item.getQuantity()); orderItems.add(orderItem); } // 保存订单和明细 orderMapper.insert(order); orderItemMapper.insertBatch(orderItems); return order; }注意事务注解必须加上。如果创建订单过程中任何一步失败所有操作必须回滚不能出现订单没建成功但库存已经被扣了的情况。这一点在答辩时一定要主动强调。库存扣减的并发问题超卖我会在下一章详细讲。3.5 后台数据统计与图表可视化管理端一个重要的加分模块是数据统计大屏。如果时间充裕务必做上因为答辩时图表展示效果比文字描述强太多。数据统计接口建议提供今日订单量、今日销售额近7天/30天订单趋势商品类目销量占比各摊位销售额排行后端实现就是聚合查询SELECT DATE(create_time) AS day, COUNT(*) AS orderCount, SUM(total_amount) AS totalAmount FROM orders WHERE create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time) ORDER BY day;前端用ECharts渲染折线图和饼图。这里有一个小技巧把统计接口返回的数据结构设计成前端直接可用的比如“日期列表数值列表”而不要返回一堆对象让前端自己转。这能省很多联调时间。4. 踩坑实录并发、部署与排查技巧4.1 跨域与Token携带问题前后端分离项目跨域是第一个绕不过去的坑。前端跑在8080端口后端跑在9090端口直接调用接口必然被浏览器拦截。解决方案是后端配置CORS过滤器允许特定来源跨域访问。同时要注意前端在axios请求里要带token到header里如果自定义了header字段需要在CORS配置里加上暴露的header名称。Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.setAllowedOriginPatterns(Collections.singletonList(*)); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }踩坑提示allowCredentials(true)和allowedOrigins(*)不能同时使用否则部分浏览器会报错。用setAllowedOriginPatterns(*)可以绕过这个问题。4.2 超卖问题乐观锁与Redis方案对比“超卖”是交易系统里最经典的问题。两个用户同时下单同一件库存为1的商品如果代码里只是select查询库存再update扣减会出现两个用户都下单成功但库存变成-1的情况。原因在于“查询库存”和“更新库存”不是原子操作。解决方案有三种方案一数据库行锁。用SELECT ... FOR UPDATE把商品行锁住但性能较差不推荐。方案二乐观锁。在商品表加version字段更新时校验版本号UPDATE product SET stock stock - #{quantity}, version version 1 WHERE id #{productId} AND version #{version} AND stock #{quantity}更新影响行数为0说明库存被并发修改过本次下单失败。这个方案简单可靠适用于场景不复杂、并发量不大的毕设项目。方案三Redis预扣库存。将商品库存预先加载到Redis中扣库存时用Lua脚本保证原子性。这个方案更接近企业级做法但实现复杂度高一些。如果项目里已经在用Redis可以顺手实现答辩时是一个很大的亮点。我建议的做法是核心下单接口用乐观锁兜底同时用Redis做商品浏览的缓存。这样既保证正确性又展示了缓存设计能力。4.3 文件上传大小限制与路径映射商品图片上传Spring Boot默认最大上传大小为1MB。我当时的项目需要上传商品实拍图经常报MaxUploadSizeExceededException后来在配置文件里调大spring.servlet.multipart.max-file-size5MB spring.servlet.multipart.max-request-size10MB图片上传后如果存到后端本地目录需要配置虚拟路径映射否则前端没法通过URL访问图片。另外图片文件不要存数据库数据库只存文件路径。如果部署到服务器把图片目录放在固定位置用Nginx做静态资源映射性能好很多。4.4 部署上线常见坑把一个SpringBoot项目部署到服务器上其实就是打包成jar包用java -jar跑起来。但有好几个坑必须提前准备好MySQL和Redis的地址不要写localhost要写服务器IP或者容器IP。application.properties里配置好生产环境的数据库账号密码最好用环境变量或者外部配置文件覆盖。打包前注意项目里有没有测试代码会把数据库和Redis清掉测试类加SpringBootTest容易连带启动整个Spring环境。前后端分离部署时后端只服务接口前端打包成静态文件丢到Nginx。Nginx配置里要加代理转发/api路径到后端端口同时配置前端路由history模式下的try_files否则刷新页面会404。部署这一块不用琢磨太深能把jar包在Linux服务器上跑起来、前端页面能访问、接口能通就已经超过90%的毕设了。如果非要上Docker也是加分项但要确保自己能解释清楚镜像和容器的区别别到时候被追问得哑口无言。4.5 常见问题速查表现象排查思路推荐解法前端跨域报错看CORS配置是否生效加CorsFilter注意Credentials与Origins冲突登录接口404是否放行了拦截规则在SecurityConfig中放行密码校验失败确认数据库中的密码是BCrypt加密不要用明文存库下单后库存不变事务是否生效加Transactional并检查类是否被Spring管理图片上传失败看报错是否超限调大multipart配置定时关单不执行是否开启EnableScheduling主类上添加开启注解JWT过期后还在操作缺少全局异常处理在全局异常处理器里捕获ExpiredJwtException5. 答辩亮点包装与项目复盘5.1 如何包装项目亮点同样的功能会交流的人答辩分能高一个档次。这套系统里至少有这几个亮点可以讲第一订单超时自动取消机制。主动讲讲你是怎么做定时任务扫描待支付订单并释放库存的。如果用了Redis的过期事件一定要讲清楚为什么比定时任务更高效定时任务需要全表扫描过期事件由Redis主动触发秒级水平不用轮询数据库。第二并发扣库存的可靠性保障。主动说明你用乐观锁版本号或者RedisLua防止超卖。这是交易系统里最核心的问题讲出来面试官会认为你理解业务和并发。第三权限体系设计。一张用户表区分三角色JWT无状态认证配合Spring Security的RBAC权限模型清晰简洁。这个体现的是对系统架构的整体把控。第四数据可视化。管理端大屏里的销量趋势、类目分布用ECharts展示说明你有数据分析和可视化意识。5.2 演示流程与答辩注意事项演示阶段建议按功能主线走一遍用消费者账号登录浏览商品、加购物车、下单、模拟支付。切换到商家账号处理订单发货修改商品库存。切换到管理员账号查看数据统计大屏、审核入驻。注意演示前把数据准备好比如给两个摊位的账号各准备几笔不同状态的订单给近7天的统计表造一些有波动性的数据。不要让当场演示变成现场调试。讲项目的时候不要只讲“我用了什么技术”要把“为什么用这个技术”讲清楚。比如为什么订单明细要存商品快照为什么用Redis做购物车为什么密码要用BCrypt加密这些问题都是答辩时的高频追问提前准备好答案能显得你是真的做了项目而不是照着教程抄的。5.3 后续可扩展方向如果时间充裕这套系统还有很多可扩展的点接入第三方支付沙箱把模拟支付换成真实支付流程。增加配送模块实现同城配送的订单分配、骑手接单。加入消息推送通过WebSocket给商家推送新订单提醒。商品推荐基于用户购买记录做简单的频次推荐。我在实际做这套系统的过程中最大的体会是增删改查本身不难难的是把交易流程的状态流转想清楚把并发情况下可能出现的边界问题处理干净。你顺着订单、库存、支付这条主线做一次整个对SpringBoot和数据库事务的理解都会上一个台阶。哪怕最后代码不完美只要逻辑闭环、思路清晰这轮实操绝对不亏。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑