每年计算机毕业设计的选题季,都有大量同学纠结要不要做"农产品销售"这类系统。我的看法是:选题不怕老,怕的是你只会照抄增删改查,讲不出设计理由,答不上来关键技术点的坑。springboot作为后端框架,搭配微信小程序做前端,再加上农产品这个非常垂直的业务场景,是一个供应链条完整、角色分明、演示效果直观、答辩时有话可说的组合。这篇文章我就从实际能落地的角度,把整个系统的业务拆解、技术设计、核心代码逻辑、以及那些文档里不会写的坑全部梳理一遍,给准备做这个题目的同学一条可以直接走的路。
1. 业务场景与需求拆解:做系统之前先把"人"和"事"理清楚
很多同学拿到这个题目第一反应是打开IDE建表写接口,这是大忌。毕业设计和企业项目的最大区别在于,毕业论文需要你讲清楚业务逻辑和技术方案之间的映射关系,如果业务都拆不明白,后面写再多代码也圆不回来。
1.1 农产品销售系统里的三方角色与核心利益
农产品销售不是普通的电商,它有很强的时间性、地域性和信任成本。消费者会关心产地是不是真实的、蔬菜是不是新鲜的、价格是不是比菜市场便宜。所以系统里的角色不能只设计成"用户"和"管理员"两个就完事,至少要拆出三个端、四种身份。
- 消费者端(微信小程序):浏览时令农产品、按分类筛选、加入购物车、下单支付、查看订单状态、确认收货、发表评价。
- 农户/商家端(微信小程序或后台管理,看时间决定):上架商品、维护库存、处理订单、查看销售统计。
- 平台管理端(web管理后台或小程序内嵌管理页):审核农户入驻、管理商品类目、处理售后纠纷、发布公告、查看全平台交易数据。
三方角色的利益点完全不同。消费者要的是"所见即所得",农户要的是"好货卖出好价",管理员要的是"平台规范有序"。系统设计时要做的,就是把这三个诉求落到具体的功能模块里去,而不是机械地堆砌CRUD。
这里有一个很容易被忽视的设计细节:农产品有"产地"和"季节"属性,这两项必须在上架时作为必填项处理。这不是为了表单好看,而是在后续做推荐、搜索、筛选时,产地和时令会是用户最在意的过滤条件。
1.2 核心业务流程的全链路梳理
整个系统的核心流程是:用户登录 -> 浏览商品 -> 加购 -> 下单 -> 支付 -> 商家接单 -> 发货(同城配送/快递) -> 用户确认收货 -> 评价。
订单状态流转是这个项目最重要的状态机,建议按下述方式设计,答辩时直接把这套状态机画出来,能比干讲代码多得5分。
| 状态编码 | 状态名称 | 触发时机 | 下一动作 |
|---|---|---|---|
| 0 | 待付款 | 用户提交订单 | 用户发起支付或订单自动取消 |
| 1 | 待发货 | 支付回调成功 | 商家在商家端点击发货 |
| 2 | 待收货 | 商家录入物流单号 | 用户确认收货或超时自动确认 |
| 3 | 已完成 | 用户确认收货 | 用户可发表评价 |
| 4 | 已取消 | 用户取消或超时未支付 | 流程结束 |
| 5 | 退款中 | 用户申请退款 | 商家/管理员审核 |
| 6 | 已退款 | 商家同意退款 | 流程结束 |
前后端传递订单状态时不要直接传中文字符串,传数字编码,前端再根据编码映射展示。这样做的原因是:第一,数字编码更省流量;第二,状态扩展时不需要改动数据库结构;第三,后端判断状态流转用switch比equals字符串更清爽。
1.3 非功能性需求:毕设不能只做"能跑"
除了功能上的需求,网上评审老师还会看一些非功能性的东西。我建议在论文的可行性分析里至少提到以下几点,这也是系统设计时的具体约束:
- 性能方面:小程序首屏商品列表加载时间不超过2秒,这个可以通过数据库索引和接口数据瘦身实现。
- 安全方面:用户密码不能明文存储,需要BCrypt加密;涉及金额的接口必须做参数校验,不能只靠前端把关。
- 兼容方面:微信小程序基础库版本要向下兼容到2.14.0左右,保证大部分用户手机能正常运行。
- 可用性方面:支付结果不能只靠前端跳转判断,必须以后端收到微信支付回调为准。
2. 技术选型与工程结构设计:springboot这个版本该怎么定
说完业务再谈技术。后端用springboot没什么悬念,但版本选择是个容易被新手忽略的坑。现在springboot已经到了3.x,但我不建议毕业设计直接用最新版。
2.1 版本选型的依据与兼容性分析
Spring Boot 3.0以上版本强制要求JDK 17,而很多学校的实验环境和学生本机装的是JDK 8。如果你选了最新版,最后部署到实验室服务器时发现环境不兼容,那就很被动了。稳妥的做法是使用Spring Boot 2.7.x版本,搭配JDK 8、MyBatis或MyBatis-Plus、MySQL 5.7或8.0。这个组合经过多年验证,网上的资料最多,报错也最好查。
另外一个选型点是MyBatis,建议直接上MyBatis-Plus。理由有三:第一,单表CRUD不用手写XML,给你节省至少两天时间;第二,分页插件内置了,不需要自己封装PageHelper;第三,字段自动填充、逻辑删除这些功能写论文时是亮眼的加分点。
前端小程序不建议用uniapp,直接使用原生微信小程序开发。理由很实际:毕设的展示环节是现场演示,原生小程序在微信开发者工具里的调试体验最直接,出了问题也好查。uniapp虽然能一套代码多端发布,但调试链路过长,对新手不友好。
2.2 后端工程目录结构:分包方式决定代码质量
后端工程的包结构我建议按"模块分包"而不是"按层分包"。很多教科书喜欢用controller、service、mapper三个包分到底,这在单体小项目里问题不大,但农产品销售系统功能模块多,订单、商品、用户各自有独立的逻辑,按层分包会导致包内文件越来越臃肿。
推荐的做法是按业务域分包:
com.farm.market ├── common // 全局返回结果、异常处理、常量 ├── config // 配置类:微信配置、MyBatis配置、拦截器配置 ├── controller // 各模块的controller,按模块拆分子包 │ ├── user │ ├── product │ ├── order │ ├── cart │ └── payment ├── service // 接口 + impl ├── mapper // MyBatis-Plus的mapper接口 ├── entity // 数据库实体类 ├── dto // 前端传参对象,与entity分离 ├── vo // 返回给前端的视图对象 └── utils // JWT工具、日期工具等entity和vo一定要分开,不要偷懒直接用实体类返回前端。原因很简单:实体类里的字段和数据库一一对应,直接暴露会把一些不该给前端看的字段(比如密码哈希、逻辑删除标记)传出去,既不安全也不规范。数据库里可能有10个字段,但前端只需要5个,vo里只放需要的字段就行,这样还能减小传输体积。
2.3 数据库设计要点:七张核心表撑起整个系统
农产品销售系统的核心表可以控制在7张左右,对毕设来说完全够用。users(用户表)、product(商品表)、category(分类表)、cart(购物车表)、orders(订单表)、order_item(订单明细表)、address(收货地址表)。如果要做评价,加一张evaluation表;要做农户管理,加一张farmer表。如下是商品表和订单表的关键字段设计,这两张表的设计质量直接决定后续开发效率。
商品表product:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name | varchar(64) | 商品名称 |
| category_id | bigint | 分类id |
| price | decimal(10,2) | 单价 |
| stock | int | 库存 |
| origin | varchar(64) | 产地 |
| image | varchar(255) | 主图URL |
| detail | text | 商品详情 |
| status | tinyint | 上架/下架 |
| create_time | datetime | 创建时间 |
订单表orders:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 订单编号 |
| user_id | bigint | 下单用户 |
| total_amount | decimal(10,2) | 订单总金额 |
| pay_amount | decimal(10,2) | 实付金额 |
| status | tinyint | 订单状态 |
| address_info | varchar(255) | 收货信息快照 |
| pay_time | datetime | 支付时间 |
| refund_time | datetime | 退款时间 |
订单编号别用数据库自增id直接展示,一定要单独生成唯一字符串,常见做法是yyyyMMddHHmmss + 用户ID后四位 + 随机数,这样看起来更真实,也能避免订单号被遍历。
3. 后端核心实现与接口设计:从登录鉴权到支付对接
后端开发的顺序建议是:先搭公共模块,再做登录鉴权,再做商品浏览,再做购物车和订单,最后做支付。支付放最后是因为它最容易出问题,需要单独留时间排查。
3.1 统一返回结果与全局异常处理:开发效率倍增器
几乎所有课程设计都不会要求做统一返回,但真实项目里这是必须的基础设施。统一返回结果的做法是封装一个Result类,包含code、message、data三个字段。
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }有了这个类,所有controller返回的格式就统一了。前端小程序封装request时只需要判断code是否为200,其他一律弹出message提示,不用每个接口单独处理错误分支。
全局异常处理要配合@RestControllerAdvice使用,把业务异常和系统异常分开捕获。业务异常指的是"库存不足""订单状态不允许取消"这类可预期的错误,自定义一个BizException,抛出时返回对应的错误码;系统异常统一返回500和兜底提示,不要把堆栈信息暴露给前端。
3.2 微信小程序登录与JWT鉴权流程
微信小程序登录和Web端的账号密码登录不一样,它的核心是基于微信的openid来识别用户。登录流程是这样的:
- 小程序端调用
wx.login()拿到临时code。 - 小程序把code传给后端接口
/api/auth/login。 - 后端用code调用微信的
jscode2session接口,换取openid和session_key。 - 后端根据openid查用户表,查不到就自动注册一个新用户。
- 生成JWT token返回给小程序,后续所有请求都在header中携带这个token。
这一步有个需要注意的细节:调用jscode2session时需要用到小程序的AppID和AppSecret,这两个配置绝对不能硬编码在代码里,更不能提交到Git仓库。正确的做法是放在application.yml中,并在.gitignore里排除掉配置文件。
后端拦截器里校验JWT的写法大致如下:
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if ("OPTIONS".equals(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { token = token.substring(7); } try { Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); return true; } catch (Exception e) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"message\":\"登录已失效,请重新登录\"}"); return false; } } }拦截器注册时要注意放行哪些路径。/api/auth/login、/api/product/list、/api/product/detail这些接口应该允许匿名访问,否则微信审核时测试人员拿不到token就进不去小程序。
3.3 微信支付V3对接:从下单到回调的完整链路
微信支付是小程序商城绕不开的模块,也是毕设里最容易翻车的部分。网上铺天盖地的教程大多还是V2版本的写法,这两年微信支付全面推行V3接口,API风格和签名算法都变了。热搜词里出现的"小程序微信支付v3对接"和"由于小程序违规,支付功能暂时无法使用",说明大家确实在这一块踩了不少坑。
支付V3的核心流程可以简化成四步:
- 后端调用微信支付V3的
/v3/pay/transactions/jsapi接口,传入用户openid、订单号、金额、回调地址,拿到prepay_id。 - 后端用prepay_id生成小程序端调起支付所需的paySign参数。
- 小程序端调用
wx.requestPayment发起支付。 - 支付成功后,微信服务器异步通知后端配置的回调地址,后端在回调里校验签名、验密、更新订单状态。
V3接口的签名方式和V2完全不同。V2用的是MD5签名,V3用的是RSA非对称签名,而且要求用SHA256withRSA算法,请求头中还必须带上Authorization头。
// 构造Authorization头(简化版,实际需要用微信商户API证书的私钥签名) String message = "POST" + "\n" + "/v3/pay/transactions/jsapi" + "\n" + timestamp + "\n" + nonceStr + "\n" + body + "\n"; String signature = sign(message, merchantPrivateKey); String authHeader = "WECHATPAY2-SHA256-RSA2048 mchid=\"" + mchId + "\",nonce_str=\"" + nonceStr + "\",signature=\"" + signature + "\",timestamp=\"" + timestamp + "\",serial_no=\"" + serialNo + "\"";这块开发时强烈建议先走通沙箱环境再切正式环境,否则真金白银试错成本太高。还有一个经常被忽略的问题:回调地址必须是公网可访问的HTTPS地址,本地开发时可以用内网穿透工具把本地服务暴露出去,但要注意穿透工具的外网域名能不能配HTTPS证书,不能的话回调就收不到。
回调处理有个幂等性问题:微信支付回调因为网络原因可能推送多次,后端处理时必须先查订单状态,如果已经是"已支付"就直接返回成功应答,不再执行重复的业务逻辑。否则同一个订单入账两次,金额就对不上了。
3.4 MyBatis自动建表与初始化数据配置
看到热搜词里有"springboot + mybatis 当表不存在自动建表",这个需求在毕设里确实常见。写论文的时候数据库脚本是一回事,但实际部署演示时每次手动执行SQL脚本很麻烦。Spring Boot可以通过配置spring.sql.init来在应用启动时自动执行SQL脚本。
spring: sql: init: mode: always schema-locations: classpath:db/schema.sql >const request = (url, method, data) => { return new Promise((resolve, reject) => { wx.request({ url: baseUrl + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + wx.getStorageSync('token') }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.message, icon: 'none' }); reject(res.data); } }, fail: (err) => { wx.showToast({ title: '网络异常,请稍后重试', icon: 'none' }); reject(err); } }); }); };登录态的token存到wx.getStorageSync里,请求时统一从storage取。如果后端返回401,可以在success回调里跳转到登录页,避免每个页面重复写失效处理。
4.2 顶部导航栏高度适配与自定义导航栏
热搜词"微信小程序顶部导航栏高度"是一个很小但特别坑的问题。默认情况下页面用的导航栏是系统自带的,胶囊按钮(右上角那个胶囊形状的按钮)在不同机型上的位置和高度都不一样,如果页面里要做自定义导航栏(比如背景色是深色的商品详情页),就需要动态计算出准确的导航栏高度。
官方提供了一套方案:用wx.getMenuButtonBoundingClientRect()获取胶囊按钮的位置和尺寸,再结合wx.getSystemInfoSync()获取状态栏高度,通过公式navigationBarHeight = (胶囊顶部 - 状态栏高度) * 2 + 胶囊高度算出导航栏的自定义高度。业内把这个公式验证过很多次,大部分机型都能正确适配。
自定义导航栏需要把页面的navigationStyle配置为custom,然后在页面顶部留出和导航栏等高的占位块。如果商品详情页要做沉浸式的大图头,这个高度计算就必须做对,否则返回按钮会被状态栏时间盖住。
4.3 购物车与下单支付流程的边界情况
购物车模块在这个项目里看起来简单,但边界情况比想象中多。比如用户把商品加入购物车后,商家在后台把价格改了,加入购物车时是10元,提交订单时可能变成12元。所以提交订单时不能直接用购物车里的价格,必须让后端根据商品ID重新查库计算总价,前端传过来的价格只能作为参考,不能作为计费依据。
下单接口需要开启事务,涉及的操作包括:创建订单主表记录、创建订单明细记录、扣减商品库存、清空购物车。任何一个环节失败,整个事务必须回滚,否则会出现订单生成了但库存没扣的脏数据。
小程序端调起微信支付的代码相对简单:
const data = await request('/api/payment/create', 'POST', { orderNo }); wx.requestPayment({ timeStamp: data.timeStamp, nonceStr: data.nonceStr, package: data.package, signType: 'RSA', paySign: data.paySign, success: () => { // 支付成功,跳转到订单详情页 wx.redirectTo({ url: '/pages/order/detail?orderNo=' + orderNo }); }, fail: (err) => { // 用户取消支付或支付失败 wx.showToast({ title: '支付未完成', icon: 'none' }); } });注意package这个字段在微信开发工具里是保留字,传给wx.requestPayment时必须写成package,但在变量命名时要用packageVal之类的名字,否则会报语法错误。
4.4 商品详情页与订单评价的实现细节
商品详情页通常包含商品轮播图、价格和库存信息、规格选择(斤/盒/个)、产地介绍、用户评价列表。农产品类目的评价对转化率影响很大,评价模块要包含评分、文字内容和图片。图片上传走wx.chooseMedia选图,再调用wx.uploadFile传给后端。
评价列表的展示要考虑图片九宫格布局,不同数量的图片对应不同的样式。这里有个体验细节:评价时间显示不要用原始的时间戳字符串,建议在服务端格式化好返回,或者前端写一个formatTime函数统一处理。
5. 从开发到毕设答辩的避坑指南
最后这部分,我把开发过程中最常遇到的问题和答辩时老师最关心的点系统性地整理出来。这些内容不是教科书里写的那种面面俱到,而是基于真实开发经验的高度浓缩。
5.1 支付功能被限制之后的临时方案
热搜词里有一句话特别真实:"由于小程序违规,支付功能暂时无法使用"。微信小程序在个人主体注册时无法开通微信支付,即便是企业主体,小程序上线后如果涉及虚拟支付或类目问题,也可能被封禁支付权限。对于毕设系统,这里有两条路可以走:
第一,在项目演示阶段用模拟支付代替真实支付。后端预留一个mockPay接口,演示时调用这个接口模拟支付成功,接口里模拟微信回调的幂等逻辑,照样把订单状态更新为"待发货"。论文里要说明这是为了演示方便,生产环境会切换到真实支付。
第二,把微信支付渠道设计成接口适配器的模式。定义一个PaymentService接口,真实支付和模拟支付分别实现,通过配置项切换。这样论文里的系统设计能体现出扩展性,答辩时也是一大亮点。
5.2 开发调试中的抓包与远程调试技巧
小程序开发时经常遇到后端接口没问题但小程序端表现异常的情况。这时候需要检查是不是缓存问题,或者是不是开启了调试模式。微信开发者工具里的"不校验合法域名"选项,在开发阶段一定要勾选,否则本地请求到不了后端。
如果你想看小程序实际发出去的请求数据,可以用抓包工具(比如热搜词里的burp suite)代理查看。抓包的关键是把微信开发者工具的代理设置为抓包工具的监听端口,同时安装对应的CA证书。要注意的是,新版微信开发者工具支持直接在Network面板里查看请求,大部分场景不需要额外抓包。
真机调试如果出现问题,优先用微信开发者工具的"真机调试2.0",它可以在手机端实时显示console日志和页面层级,比截图反馈给队友再猜问题高效太多。
5.3 微信小程序反编译与学习参考的边界
热搜词里的"微信小程序反编译"需要说清楚边界。反编译线上小程序的包,查看别人小程序的源码,这个行为本身涉嫌违反微信平台规则和知识产权。毕设阶段不建议走这条路,学习参考小程序设计应该通过正规渠道,比如微信官方文档、开源社区里的示例项目、或者同学之间互相分享自己的代码。
把反编译的时间花在读官方文档和看开源项目上,收益其实更大。微信小程序的官方文档虽然啰嗦,但每个API的边界条件和注意事项都写得很清楚,比从反编译代码里猜逻辑靠谱得多。
5.4 答辩时必问的技术问题演练
毕设答辩时,老师大概率会围绕系统设计和技术栈提问。根据经验,下面这些问题出现频率最高:
- 为什么选择springboot而不是SSH或SSM?——回答要点:springboot简化了配置、内置服务器、生态成熟、适合快速开发,重点强调自动配置原理。
- 数据库表是怎么设计的?有哪些字段?——把核心表结构讲清楚,重点说订单表和商品表之间的关联。
- JWT和Session有什么区别?为什么选JWT?——从无状态、跨端、扩展性角度回答。
- 微信支付的回调如果失败怎么办?——回答要包含幂等处理、状态机设计、人工补偿机制。
- 系统的安全性怎么保障?——从密码加密、SQL注入防护(预编译)、接口鉴权、参数校验四个维度答。
这类问题建议写成一份完整的答辩准备文档,把自己系统的每个技术决策都写成"为什么这样做"的说明,答辩前过两遍,基本就不会卡壳。
5.5 关于代码规范与注释的一个实在建议
很多同学认为代码规范是形式主义,这个观点要纠正。毕设代码要交到学校,有的学校会做查重和代码评审。如果代码里是一堆没有注释的魔法数字、命名全是a1、b2、c3,评审老师一眼就会觉得是临时拼凑的,哪怕功能全对,分数也会打折。
比较实际的规范做法是:常量用枚举或静态final变量代替,方法命名用动词开头,业务逻辑复杂的地方写清楚注释,DTO和VO字段加中文说明注解。这些不需要花太多时间,但产生的效果立竿见影。我复核代码和调试时,时间基本都花在那些没有注释的魔数上。