最近在梳理一套前后端分离的扶贫助农系统源码,里面包含了SpringBoot后端、Vue前端、MyBatis持久层、MySQL数据库这几个核心模块,还附带了完整的部署教程。这个类型的项目在毕设和课程设计里出现频率很高,但大多数人其实卡在环境和版本问题上,代码本身反而不是最大障碍。这篇文章会把整套系统的设计和跑通全过程拆开讲清楚,从数据库怎么建、接口怎么设计,到前后端怎么联调、最终怎么部署上线,一次性说透。
这套系统从功能上看并不复杂,核心就是农户、农产品、订单、后台管理这几大块。但我建议你别把它只当成一个“作业项目”来看,它里面涉及的前后端分离架构、接口设计思路、跨域处理、数据库事务这些点,放到真实的电商类项目里也完全适用。下面是正文,含项目设计解析、详细部署步骤和排坑记录,照着操作基本能把整套系统跑起来。
1. 项目定位与整体架构拆解
1.1 这个系统到底解决什么问题
扶贫助农的核心场景,是把农户的农产品直接对接到消费者手上,减少中间环节,让农产品能更顺畅地销售出去。落到系统层面,你要支撑的角色有这几类:游客可以浏览商品,注册用户能下单购买,后台管理员负责商品、订单和用户的管理。如果再加一个农户角色,就是农户可以自己上架商品、查看自己订单,这属于锦上添花,看项目需求定。
技术上这属于典型的电商基础系统,麻雀虽小五脏俱全。用户模块、商品模块、购物车模块、订单模块、支付模块(简化版)、后台管理模块,这些如果全部用原生Servlet写,代码量会非常可怕,而且前后端代码混在一起,后期维护就是灾难。这也是我选前后端分离架构的根本原因——把展示逻辑和业务逻辑彻底拆开,开发、调试、部署都能独立进行。
1.2 前后端分离的优势在哪儿
先明确一个概念。所谓前后端分离,不是说你用了Vue就叫分离,而是前端和后端在代码层面零耦合,只通过HTTP接口通信。前端跑在Nginx或者开发服务器上,后端跑在Tomcat内嵌的应用里,两边互不干扰。
这个架构带来的第一个好处是并行开发。前端工程师只需要对接接口文档,后端工程师不需要关心页面长什么样。在毕设这种场景里,也就是一个人干两个人的活,但分工清晰,写起来不混乱。第二个好处是部署灵活。前端打包成静态文件放到Nginx里,后端打成一个可执行的jar包,调整前端页面不需要重新编译整个后端项目,线上更新前端非常快。
技术选型上,后端SpringBoot是目前Java领域做单体服务的事实标准,约定大于配置,内嵌Tomcat,打jar包直接启动,不需要额外安装Tomcat。MyBatis作为持久层框架,半自动SQL模式非常适合业务SQL比较复杂、需要灵活控制SQL结构的项目。前端Vue负责数据驱动视图,配合Element UI这类组件库,管理后台页面很快就能铺出来。数据库MySQL不用多说,开源稳定,资料多,出任何问题都搜得到答案。
1.3 项目目录结构
后端部分,包结构我建议这样组织:
com.example.fupin ├── controller # 接口层,接收前端请求 ├── service # 业务层,核心逻辑 ├── mapper # MyBatis接口层 ├── entity # 实体类 ├── config # 配置类,跨域、拦截器 ├── common # 公共类,统一返回、异常处理 └── interceptor # 登录拦截器前端部分,标准Vue项目结构:
src ├── api # 接口请求封装 ├── assets # 静态资源 ├── components # 公共组件 ├── router # 路由配置 ├── store # 状态管理(Vuex) ├── views # 页面组件 ├── utils # 工具类,axios封装 └── App.vue这个结构看着简单,但实际开发中只要遵守一个原则就够了:页面和逻辑分离。页面里不要直接写大量业务代码,把逻辑抽到api和service层。后面扩展功能时,只在对应的模块里加代码就行。
2. 数据库设计与MyBatis持久层要点
2.1 核心表结构设计
数据库设计是整个系统的基础,表建得好不好,直接决定后面接口好不好写。扶贫助农系统最少需要以下这些核心表。
用户表t_user,存放用户和管理员:
CREATE TABLE `t_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(128) NOT NULL COMMENT '密码(MD5加密)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `role` tinyint(4) DEFAULT '1' COMMENT '角色:1普通用户,2管理员,3农户', `phone` varchar(20) DEFAULT NULL, `avatar` varchar(255) DEFAULT NULL, `status` tinyint(4) DEFAULT '1' COMMENT '状态:1正常,0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;商品表t_product,设计时注意价格用decimal类型,不要用float或者double,避免精度问题:
CREATE TABLE `t_product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL, `subtitle` varchar(200) DEFAULT NULL COMMENT '副标题', `main_image` varchar(255) DEFAULT NULL COMMENT '主图', `detail` text COMMENT '详情', `price` decimal(10,2) NOT NULL COMMENT '价格', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存', `sales` int(11) DEFAULT '0' COMMENT '销量', `category_id` bigint(20) DEFAULT NULL COMMENT '分类ID', `status` tinyint(4) DEFAULT '1' COMMENT '1在售,0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;订单表和订单明细表是一对多的关系。订单表记录整体信息,订单明细表记录每个商品项。
CREATE TABLE `t_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(64) NOT NULL COMMENT '订单编号', `user_id` bigint(20) NOT NULL, `total_price` decimal(10,2) NOT NULL, `status` tinyint(4) DEFAULT '0' COMMENT '0待付款,1待发货,2待收货,3已完成,4已取消', `receiver_name` varchar(50) DEFAULT NULL, `receiver_phone` varchar(20) DEFAULT NULL, `receiver_address` varchar(255) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `t_order_item` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_id` bigint(20) NOT NULL, `product_id` bigint(20) NOT NULL, `product_name` varchar(100) DEFAULT NULL, `product_image` varchar(255) DEFAULT NULL, `price` decimal(10,2) NOT NULL, `quantity` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;再补一张购物车表t_cart和一张轮播图表t_banner就足够覆盖基本功能了。注意所有表都加上create_time字段,排序和做统计时都用得上。
2.2 MyBatis配置的几个重点
MyBatis和SpringBoot整合时,配置文件里最关键的几个设置如下。下划线转驼峰必须打开,否则你查出来的字段名对不上实体类属性,除了Null什么都看不到。
mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.fupin.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl打印SQL这个配置强烈建议打开。调试阶段能直接看到MyBatis实际执行的SQL语句,定位问题会快得多。等上线前再关掉或者改为按级别控制,毕竟线上打印SQL会多一部分IO开销。
Mapper接口和XML文件我建议坚持用XML方式来做,虽然注解方式更简洁,但遇到动态SQL、多表联查时XML的表现力更强。比如商品列表按分类筛选加关键词模糊查询,用XML的<where>和<if>标签来拼接条件是最清晰的写法。
2.3 多表联查与动态SQL
商品列表查询通常会带上分类名称,这时候需要连表。我的习惯是在Mapper XML里单独写一个selectProductWithCategory语句,而不是去改基础查询。下面这段是典型的动态条件查询:
<select id="selectProductList" resultType="map"> SELECT p.*, c.name as category_name FROM t_product p LEFT JOIN t_category c ON p.category_id = c.id <where> <if test="categoryId != null"> AND p.category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND p.name LIKE CONCAT('%', #{keyword}, '%') </if> AND p.status = 1 </where> ORDER BY p.create_time DESC LIMIT #{offset}, #{pageSize} </select>注意这边分页用两个参数手动算offset,这种做法对小项目完全够用,而且不用引入PageHelper这类插件,少一个依赖就少一个坑。如果数据量上来了,再换PageHelper也不迟。另一个要注意的地方是,连表查询最好把返回类型设为map,或者在实体类里加一个categoryName的扩展字段,否则你接不到分类名这个值。
3. 开发环境准备与版本避坑指南
3.1 版本选型:千万别盲目用最新
这个项目的部署教程我整理过很多次,可以说绝大部分跑不起来的问题都出在版本选择上。现在网上各种教程和源码已经非常多了,但很多教你装最新版的坑,会让你浪费很长时间。
先说SpringBoot的版本。如果你拿到的源码是基于SpringBoot 2.x开发的,那就不要固执地换到SpringBoot 3.x。网上很多同学安装最新版SpringBoot后发现大量报错,其实是遇到版本兼容问题,缺少很多依赖和配置。SpringBoot 3.x基于Java 17,很多老教程、老依赖都没跟上。这个项目老老实实选SpringBoot 2.7.x,配JDK 1.8,是目前scenario最稳的组合。
前端Vue同理。如果你的源码是基于Vue 2写的,就不要升级到Vue 3,两者API差异很大。Element UI和Element Plus也不是一回事。我见过为了“用新版”导致整个项目跑不起来的例子,非常遗憾。这个项目如果是Vue 2 + Element UI的搭配,Node版本最好用14或者16,不要一上来就装最新的Node 20+,会有兼容性问题。
3.2 环境安装顺序与验证方法
按照下面这个顺序装环境,每一步都能快速验证是否成功,出问题时定位也容易。
JDK 1.8装好后,终端执行java -version验证,能看到1.8版本号就对了。Maven 3.6.3解压后,配置settings.xml里的本地仓库路径和阿里云镜像,执行mvn -v验证。注意Maven一定不要用IDEA自带的默认配置,本地仓库路径不统一,后面会出现jar包重复下载的问题。
MySQL装完后,核心重点工作就是初始化密码和确认端口。本地直接用mysql -uroot -p能登录就说明服务正常。使用Navicat连接数据库后,新建一个数据库,然后把项目提供的init.sql脚本导入,这一步很重要,一定不要跳过。很多同学实际跑起来才发现很多表不存在导致页面报错,后面排查会非常痛苦。
Node环境就很简单了,安装Node 14或16版本,执行node -v和npm -v验证。npm默认源在国内很慢,建议在用户目录下配置:
npm config set registry https://registry.npmmirror.com3.3 最常见的安装报错
环境配置过程中的报错大多有三种。一种是端口占用,MySQL启动不了,可以先去看看MySQL服务是不是已经存在,比如之前装过卸掉又装的,注册表残留会导致启动失败。Windows上这个问题很高频,卸载时一定要清干净。
第二种是Maven下载依赖特别慢或者直接卡住。这个基本都是镜像源没配,阿里云镜像配好后速度会提升非常明显。另外如果jar包下载到一半断网,本地仓库会出现.lastUpdated结尾的文件,这个时候要把C:\Users\你的用户名\.m2\repository下对应目录删掉重新下载,不然会一直报找不到依赖。
第三种是前端npm install报各种依赖冲突。vue2项目如果报ERESOLVE的版本树冲突,执行:
npm install --legacy-peer-deps这个参数能绕开npm 7以上引入的严格依赖树检查。很多老项目没有更新过依赖版本,就会有这个问题。
4. 后端核心业务实现要点
4.1 统一返回与全局异常处理
前后端分离的项目,接口返回格式一定要统一。我见过很多项目每个接口返回的数据结构都不一样,前端每个方法都得单独处理数据格式,非常容易乱。这个项目的做法是定义一个统一返回类:
public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.code = 200; r.msg = "操作成功"; r.data = data; return r; } public static <T> Result<T> error(String msg) { Result<T> r = new Result<>(); r.code = 500; r.msg = msg; return r; } }所有接口的返回类型都使用这个Result,前端res.data.code就能判断请求成没成功。接口从Controller返回时不要直接返回HashMap或者裸对象,全部包一层,这个习惯坚持住,前端联调时你会感谢自己。
全局异常处理用@RestControllerAdvice配合@ExceptionHandler来做,一把梭捕获所有业务异常,避免异常信息直接暴露给前端。全局异常类的好处是逻辑清晰,统一处理之后你不用在每个Controller里写try-catch。
4.2 登录鉴权与拦截器
登录模块是这个系统最核心的一个点。密码要先做不可逆加密再入库,不要存明文。项目里通常使用MD5加密,实际项目一般都升级到BCrypt。MD5属于不可逆的散列算法,做毕设够用,但如果要往生产方向走,建议换成BCrypt加盐方式。
登录成功后,后端生成token返回给前端,前端存储到localStorage里,每次请求时在请求头里带上。后端用一个拦截器统一校验token:
@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (StringUtils.isBlank(token)) { throw new BusinessException("请先登录"); } // 校验token,解析用户信息,存入request return true; } }配置类里把不需要登录的路径排除掉,比如登录接口、商品列表、商品详情这些公开接口。其他接口全部走拦截器,这样前后端分离之后,后端也不用到处写判断用户是否登录的逻辑。
按接口职责划分,购物车、下单、查看订单这些接口都要登录后才能操作。商品列表接口是公开的,游客也能看。拦截器路径配置:
// 放行路径 "/user/login", "/user/register", "/product/**", "/category/**", "/banner/**" // 拦截路径 "/cart/**", "/order/**", "/user/info", "/admin/**"4.3 下单与库存扣减的事务处理
下单流程是电商系统的核心,也是最容易出错的地方。流程是这样的:前端把购物车选中的商品信息发过来,后端根据商品ID查出商品信息,计算总价,扣库存,生成订单主表和订单明细。这里最大的坑是库存扣减。如果用户同时下单,两个请求同时读到库存都是10,然后都减1,就会出现超卖的情况。
简单方案是在SQL层面直接扣减:
UPDATE t_product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}这条SQL返回影响的行数,如果影响行数为0,说明库存不够,直接抛出业务异常。这种方式利用数据库行锁机制,能自动避免超卖。我见过很多新手是先把stock查出来,在Java代码里减1,再update回去,这种写法有严重并发问题,一定不要学。
下单过程涉及多张表的写入,必须加上@Transactional事务注解。一旦任何一个环节出问题,整笔订单回滚,保证数据一致性。事务测试也很重要,可以在中间故意抛一个异常,看数据是否回滚,证明事务生效。
文件上传方面,这个项目的商品图片上传建议存在本地目录即可,不要用Base64存数据库。新建一个上传目录,通过Nginx做静态映射访问。毕设项目不需要上OSS,等真正上生产再考虑云存储。
5. Vue前端从零跑通
5.1 前端项目结构与依赖安装
拿到了前端源码之后,你会发现里面有个package.json文件。这就是前端项目的依赖清单。在项目根目录打开终端,执行:
npm install --legacy-peer-deps等待依赖全部装完。如果安装过程中有报错,优先看是不是网络问题或者版本问题。依赖安装完成后,执行npm run serve或者根据package.json里scripts配置启动开发服务器。默认端口一般是8080,如果这个端口被占用,Vue会提示你选择另一个端口,按提示操作即可。
前端项目里面有一层很重要的目录叫api,这里面的文件是对后端接口的统一封装。比如这个项目里有一个product.js,里面是对商品接口的封装,有一个login.js,对应登录注册接口封装。你拿到源码后,打开这些文件就能看到后端接口的完整路径,拿这个去对后端的Controller,很快就能搞清楚接口结构。
5.2 axios封装与跨域
前端所有请求都是通过axios发起的。我在utils目录下放了一个request.js用来统一处理请求配置,包括baseURL设置、请求头设置、响应拦截器。这个封装的核心代码逻辑不复杂,核心作用在于:不用在每个页面重复设置请求头,可以统一处理token,还可以统一处理接口返回的错误状态。
跨域问题需要说明一下。前端跑在8080端口,后端跑在9090端口或者其他端口,两个端口不同,浏览器会拦截请求。解决办法有两个:一是后端加跨域配置,允许指定来源访问;二是前端用代理。前后端分离项目里最推荐的是在前端vue.config.js里配置代理:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }这样前端请求/api/product/list会被代理到后端http://localhost:9090/product/list,完美规避跨域。开发阶段推荐用这个方案,不需要后端改代码,而且看起来就像同源请求。生产部署再通过Nginx做反向代理,后面部署章节会详细讲。
5.3 页面实现与路由配置
前端页面整体分两块:用户端(门户)和管理端(后台)。用户端页面包括首页、商品列表、商品详情、购物车、订单列表、个人中心,管理端包括登录页、商品管理、订单管理、用户管理、分类管理等。
路由配置建议用懒加载方式,不要一次性把所有页面组件都加载进来。Vue2的写法:
const router = new Router({ routes: [ { path: '/', component: () => import('@/views/Home.vue') }, { path: '/product/:id', component: () => import('@/views/ProductDetail.vue') }, { path: '/cart', meta: { requireAuth: true }, component: () => import('@/views/Cart.vue') } ] })路由守卫用来做权限控制:
router.beforeEach((to, from, next) => { if (to.meta.requireAuth && !localStorage.getItem('token')) { next('/login'); } else { next(); } });管理端页面组件比较多,日常最常打交道的几个组件包括:表格、表单、弹窗、分页、上传组件。Element UI封装度很高,通过这些组件能快速搭出管理界面。管理端和用户端建议分成两个布局,后台首页就是一个带侧边栏的布局,侧边栏放菜单,内容区放组件。
6. 本地部署与线上发布全流程
6.1 本地启动四步走
本地跑起来这个系统,核心就四步:启动MySQL,导入数据库脚本;启动后端SpringBoot服务;启动前端Vue开发服务;浏览器访问前端页面。
第一步,确保MySQL服务已经启动,用Navicat连接本地数据库,创建一个数据库,把项目的init.sql导入。导入成功后检查表数量,确认无误再进入下一步。
第二步,后端项目用IDEA打开,等待Maven加载完依赖。修改application.yml里的数据库连接配置:
spring: datasource: url: jdbc:mysql://localhost:3306/fupin?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: 你自己的密码 driver-class-name: com.mysql.cj.jdbc.Driver注意MySQL 8.0以下的驱动包路径不一样,如果是MySQL 5.7,驱动用com.mysql.jdbc.Driver。然后运行启动类里的main方法,看到SpringBoot的启动日志出现“Started xxApplication”字样,就说明后端启动成功了。后端端口默认9090,可以在配置文件里改server.port。
第三步,前端项目终端执行npm run serve,看到编译成功,按提示访问本地地址。
第四步,浏览器访问前端地址,注册一个用户账号,然后测试商品浏览、加入购物车、下单整个流程。只要能正常跑通,这个项目本地开发环境就完全没问题了。
6.2 打包与Nginx部署
本地跑通是第一步,等需要交付或上线展示时,需要把前后端分别打包部署。
后端打包:在项目根目录执行mvn clean package -DskipTests,打包完成后target目录下会生成一个jar包。把这个jar包上传到服务器,命令行执行:
java -jar fupin-system.jar --spring.profiles.active=prod注意生产环境的数据库连接配置要改成服务器上的MySQL地址。如果服务器没有外网,直接执行nohup java -jar xxx.jar > log.log 2>&1 &后台运行。
前端打包:执行npm run build,打包完成后生成dist目录。把这个dist目录上传到服务器,用Nginx配置一个站点,root指向dist目录,同时配置反向代理转发到后端服务:
server { listen 80; server_name yourdomain.com; location / { root /www/fupin/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:9090/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }关键点在于try_files $uri $uri/ /index.html,这一行解决Vue路由刷新404的问题。很多人都踩过这个坑,部署之后页面刷新就没有了,就是因为少了这行配置。另一关键点是/api/的反向代理,前端请求/api/product/list,Nginx把请求转发给后端的/product/list,前端和后端保持同域,避免跨域问题。
6.3 部署后常见故障速查
整理一下这项目里最容易遇到的几个问题,值得收藏备用。
后端启动失败,端口被占用了怎么办?检查端口上是不是已经有进程占用了。后端端口9090默认的,如果启动报端口被占用,换一个端口就行。
MySQL连接报错:Access denied for user 'root'@'localhost',早期MySQL版本默认的root密码是空的或者不是123456,只需要重置密码就行。
前端页面能打开,但接口全部请求失败,一般是反向代理没配置对,或者后端服务没起。用浏览器网络面板检查请求状态,如果看到404,多半是后端路由没对上;看到500,打开后端日志看具体报错。
接口返回正常,但页面上的图片全部不显示。通常是文件上传后的静态资源没做映射。你上传的图片在服务器某个目录,没有通过Nginx映射这个目录,前端访问不到。
还有就是前后端时间格式问题,后端返回的时间字段可能和前端展示对不上,这个可以在实体类的时间字段加@JsonFormat注解统一格式。
根据我个人经验,这类前后端分离项目第一次跑起来,九成的问题都出在环境版本和路径配置上,代码本身反而是最不容易出问题的地方。如果你拿到了源码,建议先把文档里提供的数据库脚本导入,再启动后端,最后起前端,按照这个顺序排查问题会非常高效。版本选择上就用项目原来的技术栈,不要贸然升级,稳是第一位的。这个小系统跑通之后,你可以试着给它加一个支付模拟功能、加一个物流信息模块,或者做一个数据统计的大屏页面,这些扩展都能让你对前后端分离这套架构理解得更深入。希望这篇文章能帮你顺利跑通项目,少走点弯路。