简介:这是一份面向Java开发者、毕业设计学生及绿色农业信息化研究者的完整项目资源,基于SSM(Spring+SpringMVC+MyBatis)框架构建后端,结合Vue.js实现前端交互,覆盖绿色农产品推广网站的需求分析、系统设计、编码实现与论文撰写全过程。压缩包约92.25MB,文件种类以源代码、数据库脚本、毕业论文文档及项目配置说明为主,便于直接导入开发工具运行或二次扩展。目前已有76人学习下载,适合作为毕设选题参考、框架整合练习或农产品电商类项目的起步模板。资源附带详细毕业论文,从项目背景、功能模块划分、数据库设计到测试优化均有阐述,能帮助读者快速理解SSM与Vue前后端分离开发的核心思路;同时源码结构清晰,具备良好的可定制性,开发者可根据实际业务需求调整商品管理、订单流程、推广展示等功能模块。对于希望降低起步成本、快速获得可演示成果的毕业设计者而言,这是一份实用价值较高的参考资料。
1. 为什么绿色农产品推广网站可以用 SSM + Vue 落地
绿色农产品推广难,难在两头:一边是产地信息、检测报告、生长过程这些数据散落在不同系统里,消费者看不到;另一边是推广页面要频繁上新、做专题活动,后台改数据的人又不一定是程序员。所以这类网站本质上不是普通的商品展示页,而是一个“内容 + 商品 + 订单 + 溯源”四合一的信息系统,天然需要一套能管数据、能控权限、能支撑前后台分离的 Web 架构。SSM(Spring + Spring MVC + MyBatis)负责把业务规则和数据库访问收敛在后端,Vue 负责把页面交互做得像现代应用,两者配合正好覆盖了从“录入一条溯源记录”到“用户在手机上浏览产地详情”的完整链路。这篇博文就按这个组合的落地顺序,从架构拆解、数据建模、接口联调讲到部署验收,适合正在做 Java 毕业设计、或者想用 SSM 快速搭一个带后台管理的前后端分离项目的读者。
2. SSM 与 Vue 的工程边界:从三层架构到一次完整请求
2.1 三个框架各管哪一段
很多人在简历里写“熟悉 SSM”,但真被问到“Spring、Spring MVC、MyBatis 分别解决什么问题”时,容易答成“Spring 管对象、Spring MVC 管请求、MyBatis 管数据库”。这句没错,但不够具体。放在绿色农产品这个业务里,三个框架的边界可以画得更清楚:
Spring 容器掌管所有 Service 和 Mapper 的创建与装配,也掌管事务边界。比如“用户下单后扣减库存”这个动作,必须保证扣库存和生成订单在同一个事务里,Spring 的@Transactional就是干这个的。Spring MVC 是 Web 层的入口,负责把前端请求映射到 Controller 方法,做参数绑定、校验和 JSON 序列化。MyBatis 则把 Java 方法与 SQL 映射起来,让ProductMapper.selectPage()这样的调用变成真正执行的 SQL。
| 框架 | 在本项目中的核心职责 | 典型代码位置 |
|---|---|---|
| Spring | 对象装配、事务、AOP 日志记录 | service 包、applicationContext.xml |
| Spring MVC | 路由映射、参数绑定、返回 JSON | controller 包、spring-mvc.xml |
| MyBatis | SQL 与 Java 方法的映射、动态 SQL | mapper 接口、mapper XML |
| Vue | 页面渲染、路由控制、与后端接口交互 | views、router、api 目录 |
前后端分离之后,Vue 端不再关心 JSP 和模板引擎,它只认 JSON 接口。而后端 Controller 返回的不再是ModelAndView,而是用@ResponseBody直接返回对象,由 Jackson 序列化成 JSON。这个转变是理解整套工程的关键。
2.2 一次“查看农产品详情”请求的完整链路
以用户点击商品列表里的“查看详情”为例,走一遍请求路径,SSM 三层的分工就具体了。
Vue 端在ProductDetail.vue的mounted钩子里发起请求,访问/api/product/detail/12。这个/api前缀通常由开发环境的路径转发规则指向后端端口,后端的 DispatcherServlet 拦截到请求后,交给ProductController:
@RestController @RequestMapping("/product") public class ProductController { @Autowired private ProductService productService; @GetMapping("/detail/{id}") public Result<ProductVO> detail(@PathVariable Integer id) { ProductVO vo = productService.getProductDetail(id); return Result.success(vo); } }Controller 很薄,拿到参数后立刻交给 Service。这里有个常见的分层约定:Controller 不做业务判断,只负责参数接收和结果包装;真正的逻辑在 Service。
@Service public class ProductServiceImpl implements ProductService { @Autowired private ProductMapper productMapper; @Override @Transactional(readOnly = true) public ProductVO getProductDetail(Integer id) { Product product = productMapper.selectDetailById(id); if (product == null) { throw new BizException("该农产品不存在或已下架"); } ProductVO vo = new ProductVO(); BeanUtils.copyProperties(product, vo); return vo; } }这里@Transactional(readOnly = true)是个容易忽略但值得写的细节:查询方法只读,不开启真实事务,减少开销;同时一旦方法里混入了写操作,框架会在启动校验时报错,提早暴露问题。
请求继续往下到 MyBatis。ProductMapper是接口,真正执行的是ProductMapper.xml里的selectDetailById。SQL 里把商品表、产地表、检测表 join 起来,把一次页面展示需要的字段全部查出。
请求返回时数据流反向走:MyBatis 把ResultSet映射成Product对象,Service 转成 VO,Controller 包一层Result<T>,Jackson 序列化成 JSON,Vue 拿到后在页面渲染。这就是一次请求完整经过 SSM 的全部路径。
2.3 为什么这个题目选 SSM 而不是 Spring Boot
这是个绕不开的问题,尤其是现在很多课程设计都用 Spring Boot。我的看法是:SSM 仍然是理解 Java Web 知识体系的“最小完整样本”。Spring Boot 把自动配置、内嵌 Tomcat、Starter 依赖都藏了起来,学生写起来爽,但答辩被问到“Spring 容器是怎么启动的”“MyBatis 的 SqlSessionFactory 从哪来”时容易卡壳。SSM 则把这些配置显式地摆在web.xml、spring-mvc.xml、mybatis-config.xml里,每一行都能对上一条知识点。
另外,这套工程的代码结构对调试和学习也更友好。改动一张表,你能明确感知到自己动了哪一层;接口报错,堆栈能精确指到 XML 里的哪条 SQL。等把 SSM 这套跑通了,再去看 Spring Boot 自动配置的原理,属于先见树木再见森林;反过来直接上手 Boot,遇到问题反而容易一头扎进黑盒里。
3. 绿色农产品业务中的数据建模与 SSM 三段式编码
3.1 从业务里抽出六张核心表
绿色农产品推广网站和普通电商网站的表结构差异不大,但多了一块“溯源”内容。溯源信息是这类网站建立信任的关键,所以数据模型里必须有一张专门存追溯记录的表。一个通用性较好的模型包含六张表:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| user | id, username, password, role | 前台用户与后台管理员共用,role 区分 |
| category | id, name, parent_id | 农产品分类,支持二级分类 |
| product | id, category_id, name, price, stock, status, origin | 商品基本信息,status 控制上下架 |
| trace_record | id, product_id, batch_no, grow_place, detect_report | 每批次农产品的溯源信息 |
| cart | id, user_id, product_id, quantity | 购物车,不落库也可以,但落库方便续传 |
| order_info + order_item | 主从两表 | 拆主从表是为了让订单结构更清晰 |
这六张表能给答辩现场的 ER 图提供完整素材,也能支撑起“用户下单→扣库存→生成溯源订单”这样的业务闭环。设计时注意,product表里的status字段建议用tinyint而不是varchar,代码里定义常量0 下架 / 1 上架 / 2 售罄,前端展示时再映射成对应文案。
3.2 用 MyBatis 动态 SQL 实现“分类筛选 + 关键字搜索 + 分页”
列表查询是后台管理系统里最常写的功能,也是 MyBatis 动态 SQL 最典型的应用场景。用户在前台按“绿色蔬菜”分类、输入“有机”关键字,请求进入后端时,三个条件里可能有一个为空,动态 SQL 可以优雅地处理这种情况。
<select id="selectProductPage" resultType="com.demo.pojo.Product"> SELECT id, category_id, name, price, stock, origin, create_time FROM product <where> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> AND status = 1 </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select><where>标签会在子元素都不成立时自动去掉整个 WHERE 关键字,在有条件时自动去掉第一个多余的AND;CONCAT('%', #{keyword}, '%')避免手动拼接引号带来的注入风险。LIMIT使用offset和pageSize两个参数控制分页,分页参数由 Service 层根据页码计算:
public PageResult<Product> queryPage(ProductQuery query) { int offset = (query.getPageNum() - 1) * query.getPageSize(); List<Product> list = productMapper.selectProductPage(query, offset, query.getPageSize()); int total = productMapper.countProductPage(query); return new PageResult<>(list, total); }分页必须配合count查询才能拿到总条数,前端“共 48 条 / 共 5 页”的页码渲染依赖这个返回值。如果只回传列表不回传总数,分页组件功能就是残缺的。这个细节在代码评审时经常被提问。
3.3 Service 层事务:下单扣库存如何保证不超卖
绿色农产品在推广活动里经常做限量抢购,库存扣减是这类系统最有技术含量的点。先看一个相对安全的写法:
@Override @Transactional(rollbackFor = Exception.class) public Integer createOrder(OrderCreateDTO dto) { Product product = productMapper.selectByIdForUpdate(dto.getProductId()); if (product == null || product.getStock() < dto.getQuantity()) { throw new BizException("库存不足"); } int rows = productMapper.deductStock(dto.getProductId(), dto.getQuantity()); if (rows == 0) { throw new BizException("扣减失败,请重试"); } Order order = new Order(); order.setUserId(dto.getUserId()); order.setProductId(dto.getProductId()); order.setQuantity(dto.getQuantity()); order.setStatus(0); orderMapper.insert(order); return order.getId(); }两个关键点:selectByIdForUpdate加行锁避免并发读同一份库存;productMapper.deductStock的执行条件是WHERE stock >= quantity,返回影响行数为 0 时说明库存已经被其他请求改掉,直接抛异常回滚。这两层防护同时生效,可以应对绝大多数抢购场景。
这里顺便提一下@Transactional(rollbackFor = Exception.class)为什么必须写:Spring 默认只对RuntimeException回滚,如果自己的BizException继承自Exception,不写rollbackFor会导致事务只提交不回滚——库存扣了,订单没生成,这是最典型的线上事故。
4. Vue 端配合 SSM:路由、请求封装与联调
4.1 用 Vue Router 组织前台与后台两套页面
前台是给消费者看的,后台是给管理员用的,两者页面风格完全不同,建议在路由层面直接分开。用children嵌套路由,配合路由元信息meta控制访问权限:
const routes = [ { path: '/', component: HomeLayout, children: [ { path: '', name: 'Index', component: Index }, { path: 'product/:id', name: 'ProductDetail', component: ProductDetail }, { path: 'cart', name: 'Cart', component: Cart } ] }, { path: '/admin', component: AdminLayout, meta: { requiresAuth: true, role: 'ADMIN' }, children: [ { path: 'product/list', component: ProductList }, { path: 'order/list', component: OrderList }, { path: 'trace/add', component: TraceAdd } ] } ]/admin的父级路由加meta.requiresAuth,配合后面的全局前置守卫做登录校验。前台页面尽量用hash模式而不是history模式,这样部署到 Tomcat 后用户直接刷新/product/12不会出现 404,省去配置服务端回退的麻烦——对课程设计来说,这不是偷懒,是减少一类经典问题。
4.2 统一封装 axios:携带 token 与错误拦截
前后端分离后,接口鉴权通常靠请求头里的 token。如果每个请求都手动加一遍,代码会非常散。常见做法是封装一个request.js,把 token 注入和错误处理收敛到拦截器里。
import axios from 'axios' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { alert(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } ) export default requestbaseURL: '/api'配合开发服务器转发,前端代码里不需要写死后端端口;生产环境再把/api反向映射到后端即可。响应拦截器里code !== 200的统一处理,让业务方法里不用到处判断result.success,但要注意后端返回的 HTTP 状态码与业务码必须分开,登录失效用 HTTP 401,业务错误用 200 + 业务码,两者不要混用。
后端对应的登录拦截器,在 Spring MVC 里用HandlerInterceptor实现,放在spring-mvc.xml里配置拦截路径,放行/api/user/login、/api/product/**等公开接口:
public class AuthInterceptor 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); return false; } return true; } }前端跳登录、后端返回 401,两者通过同一个 HTTP 状态码对齐,前后端协作时先把这个约定写在接口文档里。
4.3 联调阶段的两个配置:跨域与开发服务器转发
本地开发时前端跑在 Vite 或 Vue CLI 的端口(如 5173),后端跑在 Tomcat 的 8080,两者不同源,浏览器会拦截跨域请求。解决思路有两个方向:后端开启 CORS,或者前端把/api转发到后端端口。我一般两个都配,前端改起来即时生效,后端配置保证部署到同一域时兜底。
后端开启 CORS,在 Spring MVC 的配置类里:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:5173") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowCredentials(true) .maxAge(3600); } }前端 Vite 的开发服务器配置,把/api开头的请求指向后端:
export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } } })rewrite这一步很关键,后端接口路径里没有/api前缀,如果不去掉,请求会到http://localhost:8080/api/product/list,而后端映射是/product/list,当场 404。这一行配置是前后端联调最常见的坑之一。
5. 源码交付前必做的部署与验收检查
5.1 用 Maven 打 war 包部署到 Tomcat
SSM 项目常见的交付物是war包。在项目根目录执行:
mvn clean package -DskipTests打好的war文件在target/目录下,放到 Tomcat 的webapps/,启动 Tomcat 后自动解压部署。-DskipTests跳过测试,课程设计阶段如果没写单元测试,用这个参数可以避免 Maven 因测试类编译失败而中止打包。
前端部分单独构建,npm run build生成dist目录。如果希望最终只有一个 war 包,可以把dist里的文件复制到src/main/webapp下再重新打包;如果前后端分开部署,则把dist交给 Nginx 托管,Vue 的baseURL改成实际的后端域名。课堂演示场景一般选合并部署,一个 Tomcat 端口全部搞定。
源码包里应包含sql初始化脚本,数据库连接信息集中在jdbc.properties。检查这份源码时,可以顺手确认jdbc.properties里数据库地址是localhost还是某个内网 IP,如果写死了一个考试环境地址,别人跑起来必然报连接失败。
5.2 五个高频问题与处理建议
Tomcat 启动正常,页面中文乱码。检查server.xml里 Connector 的URIEncoding="UTF-8",同时确认characterEncodingFilter在web.xml里url-pattern为/*且forceEncoding设为true。MySQL 连接串加characterEncoding=utf8,基础配置乱码问题一般出在这三处的某一处。刷新页面直接 404。Vue 用了history模式时,前端路由是前端控制的,后端没有对应的物理文件。两个方案:路由改成hash模式;或者在后端加一个ErrorPage把非接口路径回退到index.html。课程设计建议换hash模式,改动最小。接口返回 401,但明明登录了。大概率是 token 的传递链条断了:登录接口返回了 token,但前端 localStorage 里存的 key 名和后端拦截器读取的Authorization不一致。检查request.js里config.headers['Authorization']和后端request.getHeader("Authorization")的大小写和值格式是否完全一致。列表页报 JSON 序列化错误。MySQL 某些字段为null时,Jackson 默认会尝试输出null,如果实体类里有Date字段且为null,加上@JsonFormat有时仍会报错。常见做法是在字段上加@JsonInclude(Include.NON_NULL)或全局配置spring.jackson.default-property-inclusion。时间字段差了 8 小时。数据库存的create_time和前端显示的不一致,原因是 Jackson 默认按 UTC 序列化。在spring-mvc.xml里配置 Jackson 的timeZone为GMT+8,或者给时间字段统一加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")。这类问题排查成本很低,但“显示时间不对”在答辩演示时非常显眼。
5.3 用一张验收用例单判断项目是否完整
在提交源码前,用下面这套用例快速过一遍,不需要自动化测试工具,打开浏览器手动操作即可:
| 用例 | 预期结果 |
|---|---|
未登录直接访问/admin/product/list | 跳转登录页 |
| 普通用户访问后台路由 | 提示无权限并跳转首页 |
| 前台列表页按分类筛选 | 地址栏参数和列表结果同步变化 |
| 下单数量超过库存 | 提示“库存不足”,库存不变 |
| 修改商品价格后重新打开详情页 | 显示新价格,无缓存旧值 |
最后补一个可执行的技巧:部署完成后,用 Postman 导出一份接口集合,把登录、列表、详情、下单这 4 个核心接口加上断言,比如“下单接口返回 code 必须为 200”“未登录请求必须返回 401”。每次改完 Mapper XML 或调整了拦截器,先跑一遍断言集合再手工点页面,能在提交前拦截掉大半回归问题。
本文还有配套的精品资源,点击获取