简介:这是一套面向高校毕业设计场景的助农扶贫微信小程序完整开源项目。针对传统APP开发成本高、占用手机存储空间、用户卸载率高等痛点,该设计采用微信小程序+Java+MySQL技术栈,实现农户、管理员、用户三类角色:管理员管理注册农户与用户、统计订单、审核退货;农户管理商品、发货配送;用户可收藏、购买、支付并评价订单。资源共1205个文件,压缩包约35.44MB,文件类型涵盖png图片素材、vue前端页面、js脚本、java后端代码、wxml/wxss小程序界面、SQL数据库脚本等;同时附有开题报告、毕业论文、答辩PPT和使用说明,导入开发者工具即可运行体验。目前已有219人学习,适合计算机相关专业学生作为毕业设计参考或二次开发基线。整份资料目录结构清晰,环境配置、前后端代码与说明文档分层放置,既能帮助从零搭建项目,又能支撑开题、中期、答辩等各阶段材料,可显著降低毕业设计准备成本。
1. 助农扶贫微信小程序:为什么值得照着复现
做农产品销售的毕业设计,最容易被卡住的不是页面布局,而是从下单到退货的完整状态链路。这套助农扶贫微信小程序,用微信小程序做用户入口,后端用 Java 与 MySQL 支撑,把农户上架、用户下单、微信支付、商家发货、确认收货、评价、退货整条业务线都落到了代码里。它不是那种只有登录注册的演示项目,而是一个能直接讲清楚前后端交互的毕业设计级模板,源码、数据库脚本、开题报告、论文和 PPT 都齐全。如果你在找一份能复现交易流程的 Java 毕业设计源码,或者想快速搞懂小程序后端怎么写,这套项目值得花一个周末拆一遍。
2. 系统设计与小程序端:三个角色各自的业务流
2.1 三个角色三条线:管理员、农户、用户各管什么
助农扶贫小程序的核心是撮合农户和用户,但代码里不能只有一个商品列表。按照毕业设计的通用划分,这个系统拆成三个角色:管理员、农户、用户。三个角色在小程序端共用同一套页面框架,进入后看到的功能完全不同。用户打开小程序能浏览商品、收藏、加购物车、下单、支付、评价、申请退货;农户登录后管理自己的商品、处理订单发货、填写配送信息、回复评价、审核退货;管理员在后台负责农户入驻审核、商品上下架审核、用户信息管理、订单统计和交易金额统计。
这样的角色划分直接影响数据库表设计:用户表、农户表、商品表、订单表都要带上角色归属字段,而不是混在一张 member 表里。我的习惯是先画一张权限矩阵再写代码:什么人能调用哪个接口,在 Controller 层用什么注解拦截,在页面端用什么按钮控制,都从这张矩阵来。很多新手翻车的原因正是角色划分不清,用户加购没问题,但用户居然能改商品价格,或者农户能给自己刷好评,就是因为后端接口没有做角色校验。
从数据流的角度看,三个角色又有交会点。用户下单后,订单属于某个农户;农户发货后,用户确认收货;用户申请退货,又要回到农户审核。也就是说,订单表是三条业务线交汇的公共区域,这也是为什么后面第 4 章要把订单状态机单独讲清楚。
2.2 小程序页面清单与后端接口映射
小程序端是原生微信小程序,目录结构按页面拆。我把主要页面和后端接口对应关系列成一张表,先建立整体观感。这张表也可以直接写进论文的模块设计章节。
| 页面 | 核心功能 | 对应后端接口(示意) |
|---|---|---|
| 小程序首页 | 轮播图、推荐商品、分类入口 | GET /api/product/recommend |
| 商品列表 | 按分类检索、关键词搜索 | GET /api/product/list?categoryId=&keyword= |
| 商品详情 | 商品图、规格、库存、农户信息 | GET /api/product/detail/{id} |
| 购物车 | 加购、改数量、勾选结算 | POST /api/cart/add、PUT /api/cart/update |
| 订单确认 | 地址选择、金额计算 | POST /api/order/preview |
| 订单提交 | 生成订单、锁定库存 | POST /api/order/create |
| 支付 | 调用微信支付下单 | POST /api/pay/wechat |
| 订单列表与详情 | 按状态筛选、查看物流 | GET /api/order/list、GET /api/order/detail |
| 评价 | 订单完成后评价商品 | POST /api/review/add |
| 退货申请 | 选择订单商品申请退货 | POST /api/refund/apply |
页面和后端接口的关系,本质上是 JSON 数据契约。页面只管渲染和收集参数,业务规则全部放在后端 Service 层里。前端请求我一般统一封装一个 request.js,公共逻辑只有三件事:拼接基础 URL、附加 token、处理 401 跳回登录。这里有一个习惯值得提:不要在每个页面里直接写 wx.request,否则后端接口一换路径,你要改几十个文件。
2.3 一次完整下单:前端数据是怎么走的
把一条订单从创建到完成走一遍,就能理解前后端为什么是这么分工的。用户在小程序点立即购买时,前端先调用订单预览接口,把商品 id、数量、用户选中的收货地址 id 一起传过去,后端在这时按照数据库里的商品单价和运费规则算出应付金额返回。注意前端不能拿页面上显示的价格自己相加,因为商品价格可能被农户改过,要以数据库为准。
用户确认金额后调用订单创建接口,前端传商品 id、数量、地址 id,后端做四件事:重新查商品价格、校验库存、扣减库存、生成订单主表和订单明细表。这个动作必须包在同一个事务里,否则会出现库存扣了但订单没生成的情况。之后前端拿到订单号,再调起微信支付,支付成功后微信服务器会异步通知后端,后端把订单状态从未支付改成已支付待发货,前端这时才能跳转到支付成功页。
农户在订单列表里看到待发货订单后点击发货,填写物流公司和运单号,订单状态变成已发货。用户确认收货后可以评价;如果对商品不满意,可以申请退货,订单状态进入退货审核,农户同意后进入退货流程,管理员可以在后台看到整体的退货统计。整条链路里,前端每一步都在告诉用户现在发生什么,后端每一步都在校验这个状态能不能被改成目标状态。
3. Java后端与接口设计:从下单到发货的代码路径
3.1 工程结构与启动入口
先把工程结构放在前面,方便你导入 IDE 后快速定位。这是一个标准的 Spring Boot + MyBatis 分层工程,Maven 管理依赖。我在这个项目里习惯按功能模块组织包名,而不是按 controller、service、mapper 这种技术分层堆到底。
src/main/java └── com.example.farm ├── FarmApplication.java // 启动入口 ├── config/ // 跨域、拦截器、微信配置 ├── controller/ // 接收前端请求 │ ├── ProductController.java │ ├── CartController.java │ ├── OrderController.java │ ├── PayController.java │ ├── ReviewController.java │ └── AdminController.java ├── service/ // 业务逻辑与事务边界 │ ├── OrderService.java │ ├── ProductService.java │ └── RefundService.java ├── mapper/ // MyBatis 接口,对应 XML └── entity/ // 数据库实体类Controller 层只负责参数接收和结果包装,Service 层负责业务规则,Mapper 层只处理 SQL。这样画出来的职责边界,写论文时也容易讲。启动入口就是 FarmApplication,main 方法里通过 SpringApplication.run 启动内嵌 Tomcat,默认监听 8080 端口。
这里顺便解释一下源码包根目录里的三个 bat 脚本。1-install.bat 通常是执行 Maven 依赖安装,把本地仓库的依赖下载好;2-run.bat 用于启动后端服务,实际执行 Spring Boot 的运行命令;3-build.bat 更偏向打包,用于把项目打成可执行 jar 或部署包。下载源码后先跑 install 再跑 run,是最稳妥的顺序。
3.2 商品与购物车接口
商品列表是所有交易流程的起点。这个接口的代码风格代表了整个后端的基本写法,先看一段核心 Controller。
@RestController @RequestMapping("/api/product") public class ProductController { @Autowired private ProductService productService; @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) Long categoryId, @RequestParam(required = false) String keyword) { // 分页查询,categoryId 和 keyword 都是可空参数 return Result.success(productService.pageQuery(page, size, categoryId, keyword)); } @GetMapping("/detail/{id}") public Result detail(@PathVariable Long id) { // 商品详情同时带回农户信息,前端用于展示产地直发 return Result.success(productService.getDetail(id)); } }这段代码最值得注意的地方是分页参数。page、size 都给了默认值,前端不传也不会出错;categoryId 和 keyword 用 required = false 标成可选,表示列表页可以按分类筛选,也可以按关键词搜索。真正的查询逻辑在 productService.pageQuery 里,通过 MyBatis 动态 SQL 拼接 where 条件,不会出现把整个表查出来再内存过滤的低级写法。
购物车接口我建议用行级操作的思路来设计,而不是一次传一整个数组。添加商品就是一个 insert,修改数量是一个 update,删除是一个 delete,前端每点一次按钮,后端只处理一条记录。这样做的好处是并发场景下不容易覆盖数据,也方便在接口里加库存上限校验。购物车本质上是一个临时表,不需要设计状态机,核心字段就是用户 id、商品 id、数量、选中标记。
3.3 订单核心接口
订单模块是整个后端里最容易翻车的部分,原因在于它有状态流转。先看创建订单和发货两个核心操作。
@RestController @RequestMapping("/api/order") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/create") public Result create(@RequestBody OrderCreateDTO dto) { // 参数包含:用户id、商品id、数量、收货地址id Long orderId = orderService.createOrder(dto); return Result.success(orderId); } @PostMapping("/ship/{orderId}") public Result ship(@PathVariable Long orderId, @RequestParam String company, @RequestParam String trackingNo) { orderService.ship(orderId, company, trackingNo); return Result.success(); } }创建订单的 Service 方法必须声明事务注解,这是很多新手容易漏的点。createOrder 里要同时做三件事:锁定用户的收货地址、扣减商品库存、生成订单主表和明细表。如果没有事务,库存扣减成功但订单主表插入失败,用户会看到钱付了订单却不存在。发货接口则要校验操作人是不是这个订单所属的农户,以及订单当前状态是不是已支付待发货,状态不对直接抛出业务异常,而不是强行更新。
订单查询接口按状态筛选也要谨慎。常见的错误是用 status 等于某个具体值去做页面筛选,但订单状态机里有些状态是会并行出现的,比如申请退货中和已发货可能同时存在。更稳妥的做法是订单主表只记录主状态,退货申请单独一张表记录子状态,页面筛选主状态即可,这样不会出现一个订单既显示待发货又显示退货中的逻辑打架。
3.4 登录鉴权:wx.login 换 token
小程序端登录和网页端不一样,前端通过 wx.login 拿到临时 code,后端拿 code 去微信接口换 openid,然后生成自己的 token 返回给前端。后续请求都在 header 里带 token,后端用一个拦截器解析用户身份。
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); // 解析 token,失败则抛出 401 Long userId = JwtUtil.parseToken(token); if (userId == null) { throw new UnauthorizedException("登录已过期"); } // 把 userId 放进 request attribute,Service 层直接取 request.setAttribute("currentUserId", userId); return true; } }这个拦截器只解决是谁在请求的问题,还没有解决他有没有权限的问题。在助农系统里,用户和农户都能用同一个 token 体系,但调用发货接口时必须额外校验用户角色和资源归属。我一般把角色信息也放进 token,Service 层用当前用户 id 去查该订单归属,不匹配就直接拒绝。登录态有效期建议设置成 7 天,农户那边可以设置更长,因为他们经常隔几天才处理一次订单,token 过期会导致漏发货。
4. 数据库与订单状态机:用 MySQL 守住每一笔订单
4.1 核心表设计与职责
这个项目的数据库一共包含十几张表,核心交易链路可以浓缩成九张:用户表、农户表、商品表、购物车表、订单主表、订单明细表、收货地址表、评价表、退货申请表。每家实现细节略有差别,但选型和字段职责基本上是稳定结构。
| 表名 | 核心字段 | 作用 |
|---|---|---|
user | id、openid、nickname、phone | 小程序用户基本信息 |
farmer | id、user_id、shop_name、status | 农户身份与入驻状态 |
product | id、farmer_id、name、price、stock、status | 商品主数据,status 控制上下架 |
cart | id、user_id、product_id、quantity | 购物车临时数据 |
orders | id、order_no、user_id、farmer_id、status、total_amount、pay_time、ship_time、finish_time | 订单主表,状态机核心 |
order_item | id、order_id、product_id、product_name、price、quantity | 订单快照,价格以下单时为准 |
address | id、user_id、consignee、phone、detail | 收货地址 |
review | id、order_id、product_id、user_id、rating、content | 订单完成后评价 |
refund | id、order_id、user_id、reason、status、audit_time | 退货申请,独立于主状态 |
订单明细表必须存商品名称和单价的快照,这是容易忽略的关键点。农户改价后,历史订单不能被更新,否则对账时金额对不上。商品表里保留 stock 字段,下单扣库存时要用条件更新来避免超卖,这个在第 6 章会展开。数据库引擎统一用 InnoDB,才能保证事务和行级锁生效。
核心建表语句里,订单主表的索引设计很关键。order_no 需要唯一索引,用于防止重复订单;status 字段必须建普通索引,因为订单列表页最常见的筛选方式就是按状态查,这样订单量大了之后,状态索引加分页的性能不会太差。
CREATE TABLE `orders` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `user_id` bigint NOT NULL COMMENT '下单用户', `farmer_id` bigint NOT NULL COMMENT '所属农户', `status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态,对应状态机', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `pay_time` datetime DEFAULT NULL, `ship_time` datetime DEFAULT NULL, `finish_time` datetime DEFAULT NULL, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表';这里的时间字段我习惯用 datetime 而不是 timestamp,因为小程序后台经常要按精确时间导出报表,datetime 在 MySQL 8 里的表现更稳定。order_no 用时间戳加随机数生成,避免并发下单产生重复订单号。
4.2 订单状态机:从 0 到 8 的完整流转
订单表里的 status 字段不是随便填的,它必须遵守一张状态迁移表。这是整个项目最值得在论文里画出来的部分,也是答辩时最容易问到的地方。我用数字表示状态,避免英文长度不一致导致后期维护混乱。
| 状态值 | 含义 | 前置状态 | 触发动作 |
|---|---|---|---|
| 0 | 未支付 | 创建订单 | 用户提交订单 |
| 1 | 已支付待发货 | 0 | 微信支付回调 |
| 2 | 已发货 | 1 | 农户点击发货 |
| 3 | 已收货 | 2 | 用户确认收货 |
| 4 | 已完成 | 3 | 用户评价或自动完成 |
| 5 | 退货审核中 | 1 或 2 或 3 | 用户申请退货 |
| 6 | 退货中 | 5 | 农户同意退货 |
| 7 | 退货完成 | 6 | 管理员确认退款完成 |
| 8 | 已取消 | 0 | 用户取消或超时关闭 |
这九种状态并不是所有操作都能互相到达。比如订单在已发货状态时用户申请退货,状态会进入退货审核中,这时订单主状态不应该被覆盖成 5,而是应该保留主状态 2,同时在 refund 表里记录退货子状态。只有农户同意退货后,主状态才真正切到 6。这样设计的好处是,农户在待发货列表里不会突然找不到那笔订单。
状态流转的代码实现要遵守一个原则:更新时带上前置状态条件。比如支付回调解码后,不能直接 UPDATE orders SET status = 1 WHERE order_no = ?,而是要带上 AND status = 0。如果影响行数为 0,说明订单已经被处理过或状态异常,直接返回成功并丢弃本次通知。这种方式叫乐观校验,简单但很可靠,第 6 章讲到支付幂等时会给出完整写法。
4.3 一个订单详情查询:多表关联 SQL 样例
订单详情页要同时展示订单信息、商品明细、收货地址、评价状态,最容易写出 N+1 查询。先查订单主表,再循环查明细,数据量小没事,答辩时讲性能优化就露馅了。我一般用一条多表关联把主查询做掉。
SELECT o.order_no, o.status, o.total_amount, o.create_time, oi.product_name, oi.price, oi.quantity, a.consignee, a.phone, a.detail, f.shop_name FROM orders o LEFT JOIN order_item oi ON o.id = oi.order_id LEFT JOIN address a ON o.address_id = a.id LEFT JOIN farmer f ON o.farmer_id = f.id WHERE o.order_no = #{orderNo}这里用 LEFT JOIN 而不是 INNER JOIN,是因为订单详情页在极端情况下允许某些关联数据缺失,比如地址被用户删了但订单历史还要展示。JOIN 之后在 Java 里拼装成订单详情对象,页面直接渲染,不用再查数据库。项目里这类只读查询可以放开联表,但写操作接口必须保持单表更新,减少锁范围。
5. 部署运行与避坑指南:从三个 bat 到真机联调
5.1 三个 bat 脚本的真实分工
资源包根目录有三个 bat 文件:1-install.bat、2-run.bat、3-build.bat。先说三者的关系,再看怎么用。1-install.bat 是给 Maven 项目做依赖安装的,第一次打开项目必须先跑它;2-run.bat 是启动后端服务的,开发调试阶段用;3-build.bat 是构建部署包的,最终要丢到服务器上跑的时候用。
:: 1-install.bat mvn clean install -DskipTests :: 2-run.bat mvn spring-boot:run :: 3-build.bat mvn clean package -DskipTests这三个命令看着简单,但顺序不能乱。先 install 会把项目安装到本地 Maven 仓库,后续 run 和 build 就不需要重复下载依赖。skipTests 参数要保留,毕业设计项目通常没有完整的测试环境,跑测试反而会因为连不上测试库而失败。跑 2-run 之前确认 application.yml 里的数据库账号密码是本地配置,不然启动到一半会报连接拒绝。
我一般会在本地装一个 MySQL 8,把数据库脚本直接导入,再启动后端。前端小程序导入微信开发者工具后,只需要改一个 request.js 里的 baseURL。如果后端跑在本地,小程序开发者工具里要勾选不校验合法域名;如果是真机预览,就要把 baseURL 改成电脑的局域网 IP。这里有个细节:改了 IP 后要确保手机和电脑连同一个 Wi-Fi,并且 Windows 防火墙放行 8080 端口。
5.2 避坑:小程序端连不上后端
现象:小程序页面能打开,但所有列表都是空的,控制台报 request:fail 或者 url not in domain list。
原因:小程序有自己的域名白名单机制,直接请求 http://localhost 或者 http://192.168.x.x 都会被拦截。开发者工具默认只允许 HTTPS,本地调试时不勾选不校验合法域名根本无法访问。
解决:在微信开发者工具的详情设置里勾选不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书。如果要用真机预览,建议直接把后端接口都改成局域网 IP 可访问,并在手机上开启调试模式。后端还要注意跨域,Spring Boot 里配一个 CORS 配置允许 localhost。
提示:负责小程序端联调的同学经常忽略一件事——数据请求失败时,优先看请求 URL 本身,而不是怀疑后端代码。先用浏览器直接访问后端接口,能通就说明问题在域名校验,不能通则查后端日志。
5.3 避坑:数据库中文乱码与时区
现象:商品名称、收货人姓名插入后变成问号,或者启动报 The server time zone value 无法识别。
原因:数据库和连接 URL 的字符集与时区没有对齐。MySQL 8 默认时区是系统时区,JDBC 连接串不指定 serverTimezone 时可能无法识别中文环境。建表语句用了 utf8mb4,但连接的 characterEncoding 没设置,中文就会乱码。
解决:建库时用 utf8mb4,连接串统一改成 jdbc:mysql://localhost:3306/farm?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai。这里 characterEncoding 和 serverTimezone 两个参数必须同时出现,少一个都可能出问题。修改之后需要重启后端,注意是重启不是热部署。
5.4 避坑:下单重复与状态异常
现象:用户快速点击两次提交订单,生成两笔一样的订单;或者支付回调到达后,订单状态没有从 0 变成 1,反而停在未支付。
原因:提交订单接口没有做防重复提交,用户连点两次就创建了两条记录。支付回调没有做状态前置校验,或者回调被重复发送,导致状态被错误覆盖。
解决:前端在下单按钮上做 loading 禁用,后端在创建订单时先用 order_no 唯一索引兜底。更彻底的办法是用户在订单确认页生成一个幂等键,后端在同一事务里检查幂等键是否已存在。支付回调更新状态时必须带上 AND status = 0 的条件,影响行数为 0 时说明已经处理过,直接返回成功,不再重复更新。
5.5 避坑:后端启动失败先查数据源
现象:执行 2-run.bat 后,日志刷到一半就报 Application run failed,核心错误是 Failed to configure a DataSource。
原因:绝大多数是 application.yml 里的数据库地址、用户名、密码和本地 MySQL 不一致;也可能是 MySQL 没启动,或者数据库脚本还没导入。
解决:用数据库客户端先按 yml 里的账号密码试连,确认能通;再确认数据库里有没有订单表等核心表;最后检查 8080 端口是否被占用,Spring Boot 默认端口被其他服务占着也会启动失败。这一条排查顺序适用于大多数 Spring Boot 项目,遇到启动失败先排除环境,再怀疑代码。
6. 进阶优化:库存防超卖、支付幂等与超时关单
6.1 库存扣减:一条 SQL 解决超卖
下单并发时,先查库存再更新的写法容易超卖。我把扣减逻辑收成一条条件更新 SQL,让数据库原子地完成判断与扣减。
@Transactional public Long createOrder(OrderCreateDTO dto) { Product product = productMapper.selectById(dto.getProductId()); if (product == null || product.getStatus() != 1) { throw new BizException("商品不存在或已下架"); } int rows = productMapper.deductStock(dto.getProductId(), dto.getQuantity()); if (rows == 0) { throw new BizException("库存不足"); } // 生成订单主表和明细表,与扣库存同事务 return orderMapper.insertOrder(dto); }对应的 SQL 是 UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}。数据库行锁保证同一时刻只有一个请求能扣减成功,命中 0 行就抛异常回滚。毕业设计阶段不引入 Redis,也能把超卖问题压下去。
6.2 支付回调幂等:重复通知不重复改单
微信支付成功后会重试回调,处理接口必须幂等。我的固定做法是带条件更新状态,并维护一条支付日志。
@Transactional public void handlePayCallback(PayNotifyVO notify) { if (orderMapper.updateStatusByCondition(notify.getOrderNo(), 0, 1) == 1) { paymentLogMapper.insert(notify); } // 状态不是 0 就直接返回,微信那边会认为处理成功 }updateStatusByCondition 对应 UPDATE orders SET status = 1, pay_time = NOW() WHERE order_no = ? AND status = 0。只有真正从未支付切到已支付才算处理成功,这样重复回调不会把订单状态改乱。
6.3 未支付订单自动关闭:定时任务扫描
下单不支付的订单需要定时关单并回补库存。Spring Boot 里用 @Scheduled 扫描即可。
@Scheduled(fixedDelay = 30000) public void closeExpiredOrders() { List<Long> orderIds = orderMapper.listExpiredIds(30); for (Long orderId : orderIds) { orderService.closeOrder(orderId); } }listExpiredIds 的 SQL 条件是 status = 0 AND create_time < DATE_SUB(NOW(), INTERVAL 30 MINUTE),只扫真正超时的订单。fixedDelay 30 秒执行一次,关单方法里同样要带 status = 0 条件,防止用户刚好在超时前支付了订单。
从那以后,我每次处理带订单的小程序后端,都会强制走一遍状态机校验、乐观扣库存、回调幂等这三件套。最初觉得麻烦,后来发现上线后半夜被订单问题叫醒的次数几乎为零。这套资源把源码、数据库脚本、开题报告、论文和 PPT 都整理在一起,希望帮到你,下载后按第 5 章的顺序跑一遍,一个晚上就能把链路通起来。
本文还有配套的精品资源,点击获取