简介一份基于JavaSpring BootVueMySQL的乐乐农产品销售系统完整项目适合毕业设计、课程设计或期末大作业复用项目已通过严格调试、可直接运行。压缩包共871个文件约27.67MB包含Java/Vue前后端源码、sql数据库脚本、bat部署脚本、论文docx/doc及配套文档等前端资源以svg、js、css为主后端以java、xml为主整体目录清晰便于导入开发环境。系统围绕农产品线上销售流程界面整洁、操作直观论文详细说明设计思路、开发过程与使用方法有助于理解前后端分离开发、Spring Boot与Vue集成、MySQL数据库设计。目前已有296人学习浏览适合需要快速获得可运行电商项目范本的学生或技术人员参考实践。1. 乐乐农产品销售系统一套 javaspringbootvuemysql 的标准毕业设计长什么样乐乐农产品销售系统名字听着像个普通商城实际是 javaspringbootvuemysql 四件套攒出来的典型前后端分离毕设项目。这类打包好的“源码数据库论文”资源在网上一搜一大把但大多数人拿到手之后不是卡在启动就是搞不清表之间怎么关联更别说改代码去应付答辩。这篇笔记就照着这类项目的标准结构从建表开始一路写到前端调通把每层代码该长什么样、哪些参数最容易让你翻车讲清楚。适合两类人一类是打算自己动手把整套系统撸完的应届生另一类是已经拿到源码但还没跑起来、想快速吃透每个环节的从业者。2. 技术选型与数据库设计为什么是这套组合表要怎么拆2.1 为什么毕业设计全都挤在 Spring Boot Vue MySQL 上不是这个组合有多先进而是它刚好踩在“能运行、好答辩、老师看得懂”三个要求上。Spring Boot 用内嵌 Tomcat省去了手动配置外部容器的步骤Vue 做前端页面组件化写法让页面结构清晰MySQL 是关系型数据库里资料最多、出问题最好搜的。换成 SSM 全家桶配置文件的量翻一倍换成 PostgreSQL虽然功能更强但答辩老师不熟的东西反而容易变成扣分点。技术选型不是选最好的是选风险最小的。这套系统通常拆成两个端口跑前端 Vue 项目占用 8080 之类的端口后端 Spring Boot 占用 8081。前端通过 axios 请求后端接口后端连 MySQL。注意这种结构下跨域问题一定会遇到后面会专门说。数据库设计上农产品销售系统的核心表其实比想象的少用户表、商品分类表、商品表、购物车表、订单表、订单明细表加上公告和评论表七张表就能覆盖全部功能。2.2 核心表结构与字段边界以最常见的字段设计为例下面这张表是核心表的关系说明。订单和订单明细必须拆开因为一个订单可能包含多种农产品而商品表的库存字段在提交订单时要做减库存操作这是一张容易漏掉的关联。表名关键字段关联关系说明userid, username, password, role, phone与购物车、订单一对多role 分 buyer / seller / admincategoryid, name, sort与商品一对多农产品按蔬菜、水果、粮油分类productid, name, category_id, price, stock, image, status与分类多对一status 控制上架下架cartid, user_id, product_id, quantity与用户、商品多对一一个用户对应多条购物车记录ordersid, order_no, user_id, total_amount, status, create_time与用户多对一status 用数字表示待付款/已发货/已完成order_itemid, order_id, product_id, product_name, price, quantity与订单多对一下单时快照商品名称和价格noticeid, title, content, create_time无后台发布公告建表时最容易忽略的两个点一是 price 字段用 decimal(10,2) 而不是 floatfloat 在金额计算上会有精度误差这在农产品按斤计价、单价带小数的场景下尤其明显二是 order_no 一定要设唯一索引否则并发下单可能生成重复订单号。订单状态的数字枚举值也要在注释里写清楚比如 0 待付款、1 已付款、2 已发货、3 已完成、4 已取消论文里画状态图时直接照着写就行。2.3 前后端分离的接口约定响应体设计与请求风格前后端分离项目最怕各写各的。后端返回的 JSON 格式如果不统一前端每个页面都要单独处理错误代码会很难维护。常见的做法是定义一个统一的响应体 Result 类包含三个字段code状态码、message提示信息、data业务数据。成功时 code 为 200失败时可以是 500 或者业务自定义的 401、403。前端 axios 拦截器里只需要判断 code 等于 200 就走业务逻辑否则弹出 message。接口风格上RESTful 是主流但毕业设计里不用做得太严格。商品模块的接口通常是这样GET /api/product/list 分页查询、GET /api/product/detail/{id} 查详情、POST /api/cart/add 加购物车、POST /api/order/submit 提交订单。管理员端再单独加一组 /api/admin/ 前缀的接口。前后端联调阶段接口路径一旦定下来就不要乱改前端改一处、后端改一处联调成本会翻倍。这也是为什么先设计表、再定接口、最后写代码的顺序不能乱。3. 后端 Spring Boot 落地从骨架到一条商品接口的完整链路3.1 项目骨架与核心配置后端项目用 Spring Initializr 生成即可依赖选 Spring Web、MyBatis Framework或 MyBatis-Plus、MySQL Driver。毕业设计里用 MyBatis-Plus 的占大多数因为它自带 BaseMapper单表 CRUD 不需要写 XML 映射文件如果模板用的是原生 MyBatis那每张表都要写一个 Mapper XML工作量会大不少。我下面的示例以 MyBatis-Plus 为例。server: port: 8081 spring: datasource: url: jdbc:mysql://localhost:3306/lele_farm?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto这里最关键的是 url 里的 serverTimezone 参数。MySQL 8.x 的驱动默认时区是 UTC不加 serverTimezone后端插入订单时间会比本地时间早 8 个小时。还有 MyBatis-Plus 的 map-underscore-to-camel-case 要打开数据库的 user_name 字段才能自动映射到 User 实体的 userName 属性否则查出来的数据全是 null还不好排查。3.2 实体类与 Mapper最简单的两层MyBatis-Plus 的实体类用注解标记表名和主键策略。以 Product 为例写商品模块之前先把实体类建好后面 Controller 和 Service 才有操作对象。Data TableName(product) public class Product { TableId(type IdType.AUTO) private Long id; private String name; private Long categoryId; private BigDecimal price; private Integer stock; private String image; private Integer status; // 1上架 0下架 TableField(fill FieldFill.INSERT) private LocalDateTime createTime; }TableName 指定表名TableId 配置主键自增createTime 字段配置自动填充。实体类写完Mapper 接口只需要继承 BaseMapper 单表的增删改查就全有了。逻辑说明MyBatis-Plus 的 BaseMapper 封装了 selectById、selectPage、insert、updateById 这些通用方法不需要手写 SQL。参数说明里最需要注意的是 BigDecimal 类型对应数据库的 decimal千万不能用 Double 对应过去。3.3 Controller-Service 分层一个接口的完整写法商品分页查询是商城页面的核心接口。Controller 只负责接收参数和返回结果Service 层写业务逻辑这是分层的基本要求。常见误用是把 SQL 条件直接写在 Controller 里确实能跑但答辩被问到“Service 层的意义是什么”时很难圆场。RestController RequestMapping(/api/product) public class ProductController { Autowired private ProductService productService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 8) Integer size, RequestParam(required false) String keyword) { return Result.success(productService.pageQuery(page, size, keyword)); } }Service public class ProductServiceImpl implements ProductService { Autowired private ProductMapper productMapper; Override public PageResult pageQuery(Integer page, Integer size, String keyword) { PageProduct p new Page(page, size); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(Product::getStatus, 1); // 只查上架商品 if (keyword ! null !keyword.isEmpty()) { wrapper.like(Product::getName, keyword); } wrapper.orderByDesc(Product::getCreateTime); productMapper.selectPage(p, wrapper); return new PageResult(p.getRecords(), p.getTotal()); } }逻辑说明Controller 里 RequestParam(defaultValue 1) 给页码和每页条数设置了默认值前端没传参数时不会报 400。Service 里用 LambdaQueryWrapper 构建查询条件eq 是等值条件like 是模糊匹配orderByDesc 按创建时间倒序。参数说明Page 和 size 这两个参数直接映射前端 Element-UI 表格的 page 和 size 字段如果前端传的是 pageNum 和 pageSize这里要对应修改否则会出现“前端点第 2 页后端返回的还是第 1 页”的现象。3.4 登录鉴权JWT 拦截器与角色控制用户登录后要识别身份毕业设计里 JWT 是主流做法。用户登录成功后后端生成一个 token 返回前端前端存到 localStorage每次请求在请求头带上 Authorization 字段。后端写一个拦截器统一校验只有登录用户才能访问购物车和订单接口。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !JwtUtil.verify(token)) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } return true; } }Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtInterceptor()) .addPathPatterns(/api/cart/**, /api/order/**, /api/admin/**) .excludePathPatterns(/api/user/login, /api/user/register, /api/product/**); } }逻辑说明拦截器只拦截需要登录的路径登录注册和商品浏览不拦截。角色控制的常见做法是把角色信息写进 token 的 payload 里管理员接口再加一个注解判断。注意管理员接口切勿只靠前端隐藏入口来保护后端必须拦截 /api/admin/** 路径否则用接口工具直接调用就能绕过权限这在论文安全测试章节也是一个可以写进去的点。4. 前端 Vue 落地路由守卫、axios 封装与商城页面4.1 路由配置与登录守卫前端项目的结构一般是 Vue CLI 或 Vite 创建装好 vue-router 和 axios。路由需要分成两部分买家端页面首页、商品列表、购物车、订单和管理员端页面商品管理、订单管理、公告管理。用 vue-router 的 meta 字段标记哪些路由需要登录、哪些需要管理员权限。const router new VueRouter({ routes: [ { path: /, component: Home, name: home }, { path: /product/:id, component: ProductDetail, name: productDetail }, { path: /cart, component: Cart, meta: { requiresAuth: true } }, { path: /order, component: Order, meta: { requiresAuth: true } }, { path: /admin, component: AdminLayout, meta: { requiresAuth: true, isAdmin: true } } ] }) router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })逻辑说明beforeEach 是 vue-router 的全局前置守卫每次路由跳转前先执行。这里是从 localStorage 读取 token没有 token 就强制跳到登录页。isAdmin 的管理员判断一般需要在登录时把用户角色也存到 localStorage在守卫里加一个条件判断。注意前端守卫只是用户体验层面的控制后端接口鉴权才是真正的安全边界只有后端的 JwtInterceptor 是不够的管理员接口那侧还要做角色校验前后端两层缺一不可。如果只需要判断“是否登录”上面的写法已经够了要判断“是否管理员”还需要去调一次后端接口核对角色。这个是前端路由防跳转的常见做法但因为 token 本身是明文存储在浏览器里任何拿到 token 的人都能伪造身份。真正要防的是后端。因此我没有把完整的 admin 判断写死只把 requiresAuth 的链路放开管理员那块留给后端的 /api/admin/** 拦截去兜底。4.2 axios 封装与接口对接axios 如果裸用每个页面的错误处理都要写一遍。常见的做法是封装一个 request 模块统一设置 baseURL 和请求拦截器。import axios from axios const request axios.create({ baseURL: http://localhost:8081/api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use(res { if (res.data.code 200) { return res.data.data } else { ElMessage.error(res.data.message || 请求失败) return Promise.reject(res.data) } })请求拦截器负责把 token 塞到请求头里响应拦截器统一处理后端返回的 code。baseURL 定义的是 http://localhost:8081/api实际部署时如果前端打包后放进 Spring Boot 的 static 目录这个地址要改成相对路径 /api。具体怎么打包在部署章节里会展开。这里的参数配置有一个容易忽略的点timeout 是 10 秒如果后端接口响应超过 10 秒前端会报超时。农产品销售系统里商品列表大多是简单查询10 秒够用但如果后续加了报表导出之类的大数据量接口建议单独给那个请求配置更长的 timeout。4.3 商城页面商品卡片与加入购物车商城首页一般是一个商品列表用 Element-UI 的 el-card 组件展示农产品卡片。注意图片路径问题后端返回的 image 字段如果存的是相对路径“/upload/xxx.jpg”前端需要拼接完整的资源服务器地址。最常见的方式是后端配置虚拟路径映射把本地上传目录映射成 /upload/**前端直接使用相对路径。template div classproduct-grid el-card v-foritem in productList :keyitem.id classproduct-card img :srcitem.image alt商品图片 classproduct-image / div classproduct-name{{ item.name }}/div div classproduct-price{{ item.price }} / {{ item.unit }}/div el-input-number v-modelitem.quantity :min1 sizesmall / el-button typeprimary sizesmall clickaddToCart(item)加入购物车/el-button /el-card /div /template这个模板里 el-input-number 是 Element-UI 的数量选择器v-model 绑定了商品对象上的 quantity 属性。这里有个细节如果直接修改 item.quantity会污染商品列表数据因为 item 是后端返回的原始对象。常见做法是在商品对象上扩展一个 quantity 字段因为后端返回的数据里没有这个字段Vue 响应式系统在 v-model 绑定新增属性时可能不会触发视图更新。解决方法是先复制一份对象再绑定或者把数量选择器单独提取成子组件。参数说明里需要注意 el-input-number 的 min 属性如果不设置用户可以输入 0 或负数提交订单时后端必须再做一次数量校验前端约束和后端约束缺一不可。Vue 插槽在这个场景里也有使用场景比如 el-card 的 footer 区域放按钮。5. 部署与常见问题排查从“源码能跑”到“自己也能跑”5.1 本地启动的完整顺序拿到源码包后最容易搞错的是启动顺序。先启动 MySQL再启动后端 Spring Boot最后启动前端 Vue。如果前端先启动页面加载出来但所有数据请求都报网络错误会让人误以为是代码有问题。MySQL 这边需要注意版本MySQL 5.7 和 8.x 的驱动类名不一样如果导入的 SQL 脚本是 5.7 的导出文件直接用 8.x 的客户端导入时偶尔会出现编码问题。一般建议直接用源码包说明里指定的 MySQL 版本不要随意升级。后端启动前检查三件事数据库是否已导入、application.yml 里的数据库密码是否改成本地的、端口 8081 是否被占用。前端启动前执行 npm install如果网络不好导致依赖安装失败可以换 npm 镜像源。前端启动后访问 http://localhost:8080后端启动后检查 http://localhost:8081/api/product/list 是否返回 JSON 数据。这一步成功了整个链路才算通。5.2 常见问题避坑5 个高频翻车点翻车点一前端页面能打开但所有请求都报 404现象页面样式正常渲染控制台显示请求 http://localhost:8080/api/product/list 返回 404。 原因前端和后端端口不一致或者 baseURL 配置错误。如果前端跑在 8080后端跑在 8081axios 的 baseURL 必须写成 http://localhost:8081/api不能省略端口。 解决打开前端项目的 request 配置文件确认 baseURL 写的是后端的完整地址。如果后端跑在服务器上这里要改成服务器的 IP 或者域名。翻车点二登录接口返回 JSON但页面一直提示“未登录”现象登录成功后跳转到首页刷新页面又跳回登录页。 原因token 没有正确写入 localStorage或者路由守卫读取的 key 和后端返回的 key 不一致。比如后端返回的是 data.token前端写的是 localStorage.setItem(token)但响应拦截器已经把 code 为 200 的响应解包成 data 了取 token 时写成了 res.data.token此时 res 已经是 data 了再取 .token 就是 undefined。 解决在后端登录接口的响应拦截器里加上一行 console.log(res)看结构再取字段。一般改写成 localStorage.setItem(token, res.token) 或 localStorage.setItem(token, res.data.token)取决于拦截器是否已经解包了 data 层。翻车点三后端启动时报 Access denied for user ‘root’‘localhost’现象控制台报数据库连接失败密码错误。 原因application.yml 里的数据库密码不是本地 MySQL 的密码。很多源码包默认密码是 123456但实际安装 MySQL 时设置的密码可能不同。用 rpm 方式安装 MySQL 后root 初始密码通常写在 /var/log/mysqld.log 里而手动安装的 Windows 版 MySQL 密码是自己设的。 解决打开 MySQL 客户端确认能用自己的账号密码登录然后把 application.yml 里的 username 和 password 改成能登录的那组。如果忘了密码MySQL 8.x 可以用 skip-grant-tables 方式重置但注意重置后要立刻设置新密码并重启服务。翻车点四中文乱码现象前端页面上商品名称和公告内容显示为问号或乱码。 原因数据库连接 URL 没有加 characterEncodingutf8或者数据库表本身是 latin1 编码。 解决先在 application.yml 的 JDBC URL 里加上 useUnicodetruecharacterEncodingutf8然后检查数据库表的字符集。执行 SHOW CREATE TABLE product如果看到 charsetlatin1需要把表转换成 utf8mb4ALTER TABLE product CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci。数据库连接 URL 里的编码参数影响的是客户端与 MySQL 之间的通信编码表的字符集影响的是存储层编码两个都要对。翻车点五前端 npm install 报错或者安装完启动报 Vue 版本兼容问题现象npm install 过程中出现 ERESOLVE 错误或者 npm run serve 启动后控制台报 may not be compatible with current version of node。 原因源码包里的 Vue 版本和本机 Node 版本不兼容是主要问题。Vue2 配 Node 14/16 是常见组合如果本机装了 Node 18 及以上老项目的依赖可能编译不过。npm 的 ERESOLVE 错误经常出现在依赖树冲突老项目用 npm 7 安装时容易触发。 解决先看 package.json 里的 vue 版本。如果是 ^2.6.x建议用 Node 14 或 16 重新安装或者换用 yarn 安装依赖。npm install 加上 --legacy-peer-deps 参数可以跳过依赖冲突检查。如果是 Vue3 项目Node 16 及以上问题不大。这里没有统一的解决方案核心思路是保持本机环境和项目时代匹配。还有一种情况是源码包里的 node_modules 目录被压缩时包含了大量无关文件建议删掉 node_modules 后重新安装而不是直接解压使用。我在实际处理这类打包时Deploy 到服务器上如果服务器网络不好npm install 超时可以换成淘宝镜像源 npm config set registry https://registry.npmmirror.com 再试。5.3 打包部署前端打包放进 Spring Boot 里的完整操作如果需求是“把前端打包后直接放进 Spring Boot”也就是单端口运行需要把 Vue 的打包产物复制到 Spring Boot 的 static 目录。操作流程是三步前端修改 API 地址为相对路径 → 执行 npm run build → 把 dist 目录下的文件复制到后端 src/main/resources/static。# 前端执行打包 npm run build # 将 dist 下的所有文件复制到后端 static 目录 cp -r dist/* ../backend/src/main/resources/static/ # 后端重新打包 mvn clean package -DskipTests逻辑说明前端打包后的文件是静态资源Spring Boot 默认会从 classpath 下的 static 目录读取。但要注意 Vue Router 的 history 模式在单端口部署时刷新会 404因为 Spring Boot 不知道该把请求转发到 index.html。解决办法是在后端加一个路由重定向到 index.html 的 Controller或者把 Vue Router 改成 hash 模式。hash 模式 URL 上会多一个 # 号但刷新不会 404。毕业设计演示阶段直接改成 hash 模式就能规避这个问题history 模式是更优雅的方案但需要后端配合。参数说明复制 dist 内容时要跳过遗留的 index.html 旧文件避免后端缓存了旧版本。IDE 里按 CtrlF5 强制刷新浏览器可以解决大部分“改了没生效”的假象。6. 把“能跑”升级成“能答辩”验证方法与论文侧重点系统跑通只是第一步答辩要的是你能讲清楚每一步为什么这么做。一个可复现的验证顺序是先用管理员账号创建商品分类和商品然后用买家账号完成“搜索商品-加入购物车-提交订单-查看订单”的完整体验接着用卖家账号处理发货最后确认买家端订单状态更新。每一步都用手机拍下截图这些截图就是论文里系统实现章节的素材。如果答辩现场被要求演示这个顺序能五分钟走完且不依赖网络。论文的重心应该放在订单模块的设计说明上下单时涉及库存扣减、订单号生成、购物车清空三个动作讲清楚这三个动作的先后顺序和异常处理比堆页面截图更能体现工作量。常见做法是把订单号生成规则设计为“日期 用户ID 随机数”不用分布式 ID 方案但论文里要说清为什么这么选。最后一条习惯部署和演示前先手动重启一遍 MySQL 和后端服务确认端口没有被上一次运行残留的进程占用。这听起来像玄学但答辩现场翻车大多发生在“昨天还好好的今天突然启动失败”。希望这篇笔记能帮你把这个项目从“别人的源码”变成“你能讲清楚的作品”。本文还有配套的精品资源点击获取