楼下生鲜店卖草莓的货架上,每盒草莓上贴着一个二维码,扫一下能看到产地、采摘日期、甚至农残检测报告。我当时站在货架前就在想,这套“溯源”背后到底是怎么实现的?如果把它和商品下单、订单管理放在同一个系统里,又会是什么样?后来我毕设就真的选了“基于SSM+VUE的农产品溯源销售系统”这个题目,把一个完整的溯源+电商闭环从零做了一遍。
这篇文章想把整个选题、设计、编码、踩坑过程完整聊一遍。麻雀虽小五脏俱全,这个系统覆盖了用户管理、商品展示、购物车、订单交易、批次管理、溯源码生成和查询等多个模块,非常适合做计算机类的毕业设计、课设,也适合想系统了解SSM前后端分离开发流程的同学参考。不管你是刚定题目不知道怎么下手,还是已经写了一部分代码想看看别人的设计思路,下面这些内容应该都能给你一些实际帮助。
1. 选题底层逻辑:溯源与销售为什么是一对天然组合
1.1 先想清楚一个问题:消费者凭什么相信产品安全
做农产品电商,最核心的信任问题就是:你说你的苹果是高山果园种的,没有打农药,凭什么信你?溯源系统的本质,就是把这些“生产证据”数字化、公开化,让消费者自己去看。
如果只做溯源,不做销售,那这个系统就是个信息展示平台,没有商业闭环,数据来源全靠人工录入,场景很单薄。如果只做销售,不做溯源,那和普通商城没有任何区别,毫无选题亮点。把溯源和销售结合在一起,妙处在于:订单会“带走”溯源信息,销售过程中自然产生了消费者真正关心的那批产品数据。
具体讲,这个系统里有两条业务主线在同时跑:一条是商品流,从基地采购、入库、上架、下单、发货;另一条是溯源流,从种植批次、检测报告、采收日期,到对应批次批量生成的溯源码。两条线靠“溯源码”这个核心字段绑在一起,订单里的每件商品对应一个可查询的溯源码,消费者在订单详情里就能看到自己买到的这箱果子来自哪个基地、哪天采的、检测报告是什么。
1.2 功能模块拆解:不让模块变成摆设
我把整个系统拆成前台和后台两大部分,前台给消费者用,后台给管理员用,这两边的功能划分清晰,开发时也好分工。
| 端 | 模块 | 核心功能 |
|---|---|---|
| 前台 | 用户模块 | 注册、登录、个人信息维护 |
| 前台 | 商品模块 | 商品列表、分类筛选、商品详情 |
| 前台 | 购物车模块 | 加入购物车、修改数量、删除、结算 |
| 前台 | 订单模块 | 下单、模拟支付、订单列表、确认收货 |
| 前台 | 溯源模块 | 输入溯源码查看产品批次与检测报告 |
| 后台 | 商品管理 | 商品信息维护、上下架、库存管理 |
| 后台 | 批次管理 | 维护种植批次、基地信息、检测报告 |
| 后台 | 溯源码管理 | 批量生成码、查看码的状态与去向 |
| 后台 | 订单管理 | 查看订单、发货、订单状态流转 |
| 后台 | 用户管理 | 用户列表、启用禁用账号 |
这样设计的好处是,每一个模块都能在演示的时候讲出实际用途,不会出现“这个页面做了但不知道怎么用”的尴尬。答辩的时候老师问“为什么要有批次管理”,就可以回答:批次是溯源的单位,同一批种植、同一批检测的农产品共享一个批次信息,溯源码绑定批次,消费者查询时展示的是这个批次的完整溯源链。
1.3 适合场景:课设、毕设、小规模生鲜电商MVP
这套系统最适合做的场景就是毕业设计和课程设计。原因有两点:
其一,技术栈足够“正统”。SSM是很多学校Java课程的主线,用这套技术栈做毕设,答辩时老师容易看懂,也方便在答辩中展示自己对Spring、SpringMVC、MyBatis三者协作方式的理解。
其二,业务规模可控。农产品溯源不像工业品那么复杂,不需要考虑复杂的制造工序,核心就是“基地—批次—产品—订单—溯源码”这样一条清晰链路,数据量不大,单人完全能在几个月内完成。如果你以后想拿这个项目做创业MVP,也可以在此基础上继续扩展,比如接入真实的物流接口、对接支付网关、增加视频监控回放等,起点已经完整了。
2. 技术栈定档:SSM+VUE这套经典组合到底牛在哪
2.1 SSM三个框架的分工
很多同学学完Java,一听到SSM就头大,其实就是三个框架拼在一起,各有各的活儿。
Spring是容器,负责管理Java对象的生命周期,也就是你代码里那些Service、Mapper,不用自己new了,Spring帮你创建和维护,还顺手解决了依赖注入和事务管理。SpringMVC是接手的,处理HTTP请求,前端发来的登录请求、商品列表请求,都是先到Controller,由它把参数解析好,调用Service处理完,再把结果以JSON形式返回给前端。MyBatis负责数据库操作,它比原生的JDBC舒服得多,不用自己写复杂的连接管理,SQL写在Mapper.xml里,一个方法对应一条SQL,简单直接。
三者配合起来的链路就是:前端Vue页面点击按钮,Axios发请求到后端Controller,Controller接收参数交给Service处理,Service里若有数据存取需要就调用Mapper接口,Mapper执行SQL返回数据,数据再一层层返回给前端渲染。
2.2 为什么毕选还选SSM,而不是直接SpringBoot
很多同学会问:现在新项目不都用SpringBoot吗?为什么毕设还用SSM?
我的看法是:毕设不是追求最新,而是追求“你能讲清楚”。SSM的配置是显式的,你需要手动写Spring配置文件、SpringMVC配置、MyBatis配置,这个过程本身就是在逼你理解框架底层。SpringBoot大量使用自动配置和约定优于配置,代码确实简洁,但如果你不理解里面的原理,答辩时被问一句“SpringBoot的自动配置是怎么实现的”可能就答不上来。
反过来,做过SSM之后再去看SpringBoot,你会发现Boot其实就是把SSM这套东西做了一层封装和简化,核心容器、MVC、MyBatis集成机制并没变。所以说SSM不但是很多学校课程大纲里明确考察的内容,也是帮你建立框架思维的扎实基础。
如果学校允许用SpringBoot,那咱们另说,你大可以直接上Boot,再配上Mybatis-Plus,开发效率确实快。但如果学校有明确的SSM要求,那就踏踏实实把配置一个个写清楚,这反而是加分项。
2.3 前端为什么选VUE
Vue的优势在于渐进式框架,上手曲线平缓。毕设里面不需要掌握响应式原理、虚拟DOM这一层,会组件、路由、状态管理基本就够写页面了。
我建议使用Vue 2 + Element UI的组合,原因很现实:Element UI的表单、表格、弹窗、消息提示都封装得很成熟,后台管理页面几乎可以“拼”出来。Vue 2的资料和踩坑文章数量最多,遇到任何问题基本都能搜到解决方案。环境建议配Node.js 14左右,脚手架用Vue CLI。
整机的版本搭配可以参考这套,比较稳:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 或 11 | 大多数学校环境兼容 |
| Maven | 3.6+ | 依赖管理 |
| MySQL | 5.7 或 8.0 | 5.7在毕设中最常见 |
| Tomcat | 9.x | 配合Java 8使用 |
| Vue | 2.6.x | 组件生态成熟 |
| Element UI | 2.15.x | 表单表格组件齐全 |
| Axios | 0.27.2 | 网络请求 |
3. 数据库设计:一切业务都围绕“溯源码”这张表展开
3.1 核心表结构:不要贪多,把关键表吃透
毕设数据库设计最怕两件事:一是表建得过多,每张表都是浅尝辄止;二是表之间关系混乱,代码写起来到处拼接。我最终留下8张核心表,分别为用户表、商品表、基地表、批次表、溯源码表、购物车表、订单表、订单明细表。
把其中最关键的几张表结构列出来,你就能看出整个系统的骨架:
| 字段 | 类型 | 说明 |
|---|---|---|
| product_id | bigint | 商品ID,主键 |
| product_name | varchar(100) | 商品名称 |
| category | varchar(50) | 分类 |
| origin_base_id | bigint | 关联基地表 |
| price | decimal(10,2) | 售价 |
| stock | int | 库存 |
| image | varchar(255) | 商品图片路径 |
| status | tinyint | 0下架,1上架 |
批次表是溯源信息的载体,这里把检测报告设计成图片路径字段,这样前端展示的时候能直接显示检测报告图片,更加直观:
| 字段 | 类型 | 说明 |
|---|---|---|
| batch_id | bigint | 批次ID |
| product_id | bigint | 关联商品 |
| batch_code | varchar(50) | 批次编号 |
| base_name | varchar(100) | 基地名称 |
| plant_date | date | 种植日期 |
| harvest_date | date | 采收日期 |
| detect_report | varchar(255) | 检测报告图片路径 |
| create_time | datetime | 创建时间 |
溯源码表是整个系统的灵魂,它记录了每个码的状态以及去向:
| 字段 | 类型 | 说明 |
|---|---|---|
| trace_id | bigint | 自增ID |
| trace_code | varchar(64) | 溯源码 |
| batch_id | bigint | 关联批次 |
| product_id | bigint | 关联商品 |
| order_id | bigint | 被哪个订单消费 |
| status | tinyint | 0未使用,1已使用,2已售出 |
| query_count | int | 被查询次数 |
| create_time | datetime | 生成时间 |
订单明细表里除了常规的订单号和商品信息,还专门加了一个trace_code字段,记录这个订单项卖出时分配了哪个溯源码。这样下单和溯源就真正连在了一起。
3.2 溯源码的编码规则:既好看又好查
溯源码的生成规则我建议用有规律的编码,而不是直接用UUID,UUID太长,消费者输入体验差,而且不好讲解。我用的是“产品缩写+分类代码+日期+序列号”的格式,例如:
TB-RO-20250601-0001- TB代表特色农产品(Traceable Product)项目前缀,
- RO代表水果大类(Fruits)下的橙子子类,
- 20250601代表批次采收日期,
- 0001代表当天第1个码。
这样的编码规则有两个好处:一是消费者一眼就能看出部分信息,二是后端通过SQL模糊查询也能很方便地按日期或类别统计。实际生成时,批量插入即可。如果你们产品线比较复杂,也可以简化为“BATCH_CODE-SEQ”,直接用批次号加序号,查询时通过批次号关联批次表。
3.3 订单是怎么“带走”溯源码的
这是整个系统里最关键的业务逻辑。用户下单时,系统需要从溯源码表里取出足够数量的“未使用”状态码,把它们分配给订单明细,并且把这些码的状态改成“已售出”。消费者在自己的订单详情里就能看到溯源码,复制到溯源查询页面一搜,就能看到这个码对应的批次和检测信息。
状态流转是这样的:
码生成(status=0) → 被订单占用(status=2) → 消费者查询溯源(query_count递增)码生成后不是直接售出,而是存在库里等待被分配。为了提高演示效果,你可以额外加一个“码上架”的逻辑,比如生成了100个码,只有20个码在售,售出20个后再手动放下一批。不过对毕设来说,只要保证下单时能取出码、状态更新对就行,讲解时能把这套流程讲明白就已经是高分了。
3.4 建表时的几个实操建议
- 字符集统一使用utf8mb4,防止生僻字或一些农产品名称里的特殊符号出现乱码,比如很多水果名称里有生僻字,用utf8可能报错。
- 价格类型不要用float/double,一定要用decimal(10,2),避免金额计算精度问题。
- 不建议建物理外键。用逻辑外键维护关系就够了,物理外键在后续写代码、造测试数据时经常是麻烦来源,删除数据还要管约束,对项目开发反而碍事。
- 时间字段统一用datetime。现在新项目流行timestamp,但datetime可读性更好,展示阶段也不会被时区问题坑到。
4. 后端核心实现:三层架构里的关键代码逻辑
4.1 分层代码组织:Controller薄、Service厚、Mapper精
代码目录结构要干净。Controller只管接参数、调Service、返回统一结果,不需要写业务逻辑;Service处理核心业务,比如下单、溯源查询、订单状态流转;Mapper接口对应Mapper.xml,一个方法一条SQL。
com.example.fruitmarket ├── controller │ ├── UserController.java │ ├── ProductController.java │ ├── CartController.java │ ├── OrderController.java │ └── TraceController.java ├── service │ ├── UserService.java │ ├── ProductService.java │ ├── CartService.java │ ├── OrderService.java │ └── TraceService.java ├── mapper │ ├── UserMapper.java │ ├── ProductMapper.java │ ├── CartMapper.java │ ├── OrderMapper.java │ └── TraceMapper.java ├── entity │ ├── Product.java │ ├── Batch.java │ ├── TraceCode.java │ └── Order.java ├── vo │ ├── TraceDetailVO.java │ └── OrderVO.java └── common ├── Result.java └── AuthInterceptor.java统一返回值Result类非常重要。前后端分离开发,接口返回的数据结构越统一越好,最简单的方式是封装一个Result对象,包含code、message、data三个字段,前端拿到后直接根据code判断成功失败,不用写一堆类型判断。
4.2 溯源查询链路:从Code到完整溯源信息
溯源查询模块是答辩时最容易被现场演示的功能,必须写得干净利落。核心Service逻辑如下:
@Override public TraceDetailVO getTraceDetail(String traceCode) { // 1. 查出溯源码记录 TraceCode traceCodeEntity = traceMapper.selectByCode(traceCode); if (traceCodeEntity == null) { throw new CustomException("溯源码不存在,请确认是否为正规渠道产品"); } // 2. 更新查询次数 traceMapper.increaseQueryCount(traceCodeEntity.getTraceId()); // 3. 根据批次ID查出溯源信息 Batch batch = batchMapper.selectById(traceCodeEntity.getBatchId()); Product product = productMapper.selectById(traceCodeEntity.getProductId()); // 4. 组装VO返回 TraceDetailVO vo = new TraceDetailVO(); vo.setTraceCode(traceCode); vo.setProductName(product.getProductName()); vo.setBaseName(batch.getBaseName()); vo.setPlantDate(batch.getPlantDate()); vo.setHarvestDate(batch.getHarvestDate()); vo.setDetectReport(batch.getDetectReport()); return vo; }这个查询链路的难点不在代码本身,而在字段设计的完整性。如果当初没建批次表、没把溯源码和批次绑定,现在要补信息就得改表,很麻烦。所以数据库先行,不是一句空话。
4.3 防伪细节:查询次数提示
消费者扫描或输入溯源码后,系统除了展示溯源信息,还要告诉消费者这个码被查询了多少次。如果发现一个码短时间内被反复查询,或者查询次数异常,基本可以判断有人把这个码复制了、贴在疑似产品上。
实现起来很简单:给溯源码表加一个query_count字段,每次查询接口被调用就递增,前端在结果页显示“本码已查询 N 次”。这个功能虽然简单,但确实是溯源系统的点睛之笔,答辩时很适合主动提出来。
4.4 防并发分配溯源码:别让同一个码被卖两次
系统如果只是单机小项目,并发问题可能不会被老师追问,但下单分配溯源码时如果同一时间来了两个请求,没有约束就可能把同一个码分配给两个订单。解决方式有两种:
一种是在Java代码里加同步锁,比如给分配方法加synchronized,简单但把整个方法串行化,性能受影响;更优的方式是利用SQL条件更新,用“状态判断+更新”保证原子性:
UPDATE t_trace_code SET status = 2, order_id = #{orderId} WHERE trace_id = #{traceId} AND status = 0这条UPDATE语句的WHERE条件里带着status = 0,如果另一个请求先把它置成了2,后面的更新就会失败,返回影响行数为0,代码里以此为据重新取下一个码即可。
在Service里,把取码、更新状态、创建订单、扣库存放在同一个事务里:
@Transactional(rollbackFor = Exception.class) public Order createOrder(OrderDTO dto) { // 1. 校验库存 // 2. 从库存池中取溯源码并逐个更新状态 // 3. 创建订单主表 // 4. 创建订单明细 // 5. 扣减库存 }配合事务管理,只要任何一个环节出错,整个订单和码的状态都会回滚,不会出现“订单建了但码没分配”或者“码被占用了但订单失败”这种脏数据。
4.5 MyBatis多表查询:resultMap的典型写法
做订单列表查询时,需要关联用户表、订单明细表、商品表和溯源码表,写多表关联SQL时,MyBatis的resultMap配置要清晰。
<resultMap id="OrderVOMap" type="com.example.vo.OrderVO"> <id property="orderId" column="order_id"/> <result property="orderNo" column="order_no"/> <result property="totalPrice" column="total_price"/> <result property="createTime" column="create_time"/> <collection property="items" ofType="com.example.vo.OrderItemVO"> <id property="itemId" column="item_id"/> <result property="productName" column="product_name"/> <result property="quantity" column="quantity"/> <result property="traceCode" column="trace_code"/> </collection> </resultMap>一对多关联查询用collection标签,可以让一次查询把订单主表和明细一起查出来,避免在Java代码里循环查询造成N+1问题。这里要特别注意别名一定要和resultMap里的column保持一致,比如查询时写o.order_id AS order_id,不要又是o.order_id AS oid又映射column="order_id",对不上就全成null。
5. 前端落地与联调:把溯源链路在页面上真正“秀”出来
5.1 页面设计与路由规划
前端页面规划成两部分:商城端和管理端,两者路由分开,界面风格也可以略微区分。商城端的核心路由包括:
/home 首页 /product 商品列表 /product/:id 商品详情 /cart 购物车 /order 订单列表 /trace 溯源查询管理员端单独用一套布局,左侧菜单,右侧内容区,路由包括商品管理、批次管理、溯源码管理、订单管理、用户管理。前后端不分离的管理页面用Vue Router嵌套路由实现,嵌套路由配合侧边菜单(el-menu)很好用。
5.2 溯源结果页的“链条感”
溯源查询页是整个系统的核心展示页,视觉设计不能糊弄。输入溯源码或者从URL带参数进入页面后,页面展示两部分内容:
第一部分是产品基础信息,商品名、价格、包装图片;第二部分是溯源链路信息,用时间轴组件把“种植日期→采收日期→检测报告→出库配送”按时间顺序渲染出来。Element UI正好有Timeline时间轴组件,用起来几乎是开箱即用。
<el-timeline> <el-timeline-item :timestamp="plantDate">基地种植</el-timeline-item> <el-timeline-item :timestamp="harvestDate">果实采收</el-timeline-item> <el-timeline-item :timestamp="detectTime">农残检测 <img :src="detectReport" @click="preview" /> </el-timeline-item> <el-timeline-item :timestamp="saleTime">市场销售</el-timeline-item> </el-timeline>这样的视觉结构比单纯的表格罗列专业得多,演示时也能让老师一眼看出这个系统的特色。
5.3 axios封装与跨域处理
前端开发过程中最典型的问题就是跨域。Vue开发服务器默认跑在8080端口,后端接口跑在8082端口,浏览器会拦截跨域请求。解决跨域的常用方案是在Vue项目的vue.config.js里配置代理:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8082', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }这样前端的请求都写/api开头,开发环境下由Node代理转发到后端真实地址,浏览器感知不到跨域。在axios封装里统一设置baseURL为/api,并把token注入请求头:
import axios from 'axios' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) service.interceptors.response.use( response => response.data, error => { // 处理401等场景 return Promise.reject(error) } ) export default service这套封装基本是毕设项目的通用模板,拿过去就能用。
5.4 管理后台快速开发套路:封装通用CRUD组件
管理后台主要是增删改查,页面结构高度相似。我建议不要每张表都从头写一遍表格页,而是封装一个带搜索条件的表格页组件,将列配置、搜索字段、操作按钮传入即可。这样可以节省大量时间。
接口设计也统一遵循REST风格:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/product/list | 商品列表 |
| POST | /api/product/add | 新增商品 |
| PUT | /api/product/update | 修改商品 |
| DELETE | /api/product/delete/:id | 删除商品 |
| POST | /api/batch/add | 新增批次 |
| POST | /api/trace/code/generate | 批量生成溯源码 |
| POST | /api/order/ship | 订单发货 |
后台操作完后,前台页面立刻能看到效果,这种“立竿见影”的联动感在答辩演示时特别加分。
6. 开发过程中绕不开的五个坑
6.1 MyBatis字段映射诡异的null
做SSM项目时第一次查数据往往就遇到“查出来了,但对象属性全是null”的问题。原因大多是数据库字段是下划线命名create_time,Java属性是驼峰命名createTime,MyBatis默认不会自动转换。
解决办法有两个:一是在mybatis-config.xml里开启驼峰映射:
<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>二是在每条resultMap里显式配置字段映射。建议两件事都做:开启驼峰做兜底,关键查询用resultMap显式配置。如果你的Spring配置文件加载的是其他mybatis配置,别忘了把这个配置放在实际生效的那个文件里,别问我是怎么知道的。
6.2 LocalDateTime序列化报错
前后端交互时,如果实体里时间是LocalDateTime类型,Jackson默认序列化会报错,或者输出成一长串数组,前端拿到一脸懵。推荐在实体字段上统一加注解:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime createTime;或者更全局的做法是配置Jackson的ObjectMapper,统一注册JavaTimeModule并设置日期格式。毕设项目直接在字段上加注解最省事,注意timezone要写GMT+8,否则时间会比数据库里少8个小时。
6.3 文件上传后“过一会儿就没了”
商品图片、检测报告都需要上传。很多同学直接把图片保存到项目的src/main/resources/static/upload目录下,开发时正常,重启后图片全丢,因为打包进去的不是动态目录。正确做法是把图片保存到磁盘固定目录,比如/data/upload,然后配置虚拟路径映射:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourcePaths("file:/data/upload/"); }如果只在本地演示,直接把图片保存到Tomcat的webapps下某个目录也可以,但写论文时一定要说清楚这是个简易方案、生产环境要考虑对象存储。
6.4 Vue history模式部署404
Vue Router默认用hash模式,地址栏带个#,部署不会出问题。如果你想追求美观用history模式,开发环境没问题,打包后部署到服务器,刷新某个子路由就会404,因为服务器上没有对应的物理路径。
解决方案:要么退回hash模式,毕设完全够用;要么在Nginx里配置try_files:
location / { try_files $uri $uri/ /index.html; }如果你还是用Tomcat部署,可以配置一个转发Servlet,把所有非静态资源的请求转发到index.html。最省心还是直接用hash模式,讲清楚原理更好。
6.5 权限控制不能只靠前端
很多毕设系统的权限控制就是“前端隐藏按钮”,管理员页面对普通用户不可见,但如果你直接访问后台接口,服务器还是照常返回数据,这在评审时是不专业的。正确的做法是后端用SpringMVC拦截器:
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) { response.setStatus(401); return false; } // 校验token并解析用户 return true; } }认证方案不用搞太复杂,登录成功后用JWT生成token返回给前端,前端存在LocalStroage,请求时放入header。拦截器拦截/api/**,放行登录注册和商品浏览接口。这个设计简单、安全,也符合实际开发习惯。
7. 毕业设计与答辩:让这套系统成为你的加分项
7.1 论文(LW文档)该怎么组织才不虚
毕设论文通常要覆盖需求分析、总体设计、数据库设计、详细设计、测试等章节。很多同学代码写完了但论文不知道怎么写,其实只需把你在前面章节里做的设计决策完整表达出来。
画图是论文的加分项,用例图、E-R图、系统架构图、时序图能画就画。推荐用draw.io或者ProcessOn,画完导出图片插入文档。图不用追求过度美观,但要清晰准确。比如E-R图就把用户、商品、订单、批次、溯源码五张核心实体的关系画清楚,评阅老师一眼就能看出你理解了系统的数据流向。
论文里一定要有核心功能的文字描述,包括订单创建流程、溯源码生成流程、溯源查询流程,每个流程配文字说明和流程图。数据结构设计章节把每张表的字段定义列出来,并简要说明设计理由。
7.2 演示脚本:3分钟把亮点全部演出来
答辩现场系统演示极容易翻车,不是这里点不动就是那里没数据。一定要提前写好演示脚本,并且严格控制演示时间。我建议按这条主线走:
- 前台注册登录(证明用户模块可用);
- 浏览商品并加入购物车,提交订单(模拟支付);
- 进入订单详情,找到溯源码,复制;
- 切到溯源查询页,输入这个溯源码,展示完整溯源链路,特别放大检测报告图片;
- 切到后台管理,把刚才的订单发货;
- 展示溯源码管理页面,说明码的状态从“未使用”变成了“已售出”。
这样一条链路下来,每个模块都有上场的机会,而且有“前后台联动”的感觉,比干巴巴地展示页面有说服力得多。演示前记得把数据库恢复成有数据的干净状态,提交订单之后删掉或改掉测试数据,保证第二遍演示还能跑通。
7.3 答辩时最容易被追问的四个问题
问:为什么不用SpringBoot?答:学校课程体系里SSM是主线,SSM能更清晰地体现Spring容器、SpringMVC请求分发、MyBatis持久化三者的协作关系,写过SSM配置之后再理解SpringBoot的自动配置会更加容易。
问:溯源码防伪怎么做?答:一是查询次数监控,异常查询会提示消费者警惕;二是溯源码通过编码规则和一定的随机性生成,避免被批量猜测;三是如果进一步做真实产品,可选用一物一码的标签印刷工艺,从而在物理层面防止标签被二次复制。
问:订单量大了怎么办?答:目前系统在小规模场景下已经成熟,如果要扩展,可以在三个方向演进:引入Redis做缓存解决热点商品查询压力,引入消息队列做订单异步处理,把MySQL升级为分库分表方案。这里只需要从思路上给出方向就行,关键是体现出你考虑过系统演进问题。
问:用户密码安全怎么保证?答:不能明文存储,数据库里保存密码的MD5或BCrypt加密结果。更推荐BCrypt,因为自带随机盐,相同密码加密后的结果也不一样,能有效抵御彩虹表攻击。
最后几句实在话
如果你正在准备这个选题,我最大的体会是:不要一上来就急着敲代码,先把“溯源码从哪来、到哪里去”这条主链路在Excel或者纸面上画清楚,再动数据库表,再写后端接口,最后才做页面。数据链路理顺了,代码写起来顺滑得多,答辩讲起来也有底气。这个项目难的不是哪个技术点,而是能不能把小而完整的闭环跑通讲透。把这条链路做到位,你的毕设就已经赢过大多数同学了。