农产品自主供销小程序源码:Spring Boot+微信小程序毕设完整工程
简介:基于小程序的农产品自主供销系统源码,是面向毕业设计与课程设计的完整实践项目,覆盖管理员、农户、用户三角色,解决农户与消费者信息不对称的问题。系统采用 Java 开发,搭配 JDK1.8、MySQL5.7 与 Tomcat7 部署环境,包含首页、个人中心、用户管理、农户管理、产品分类、农产品管理、咨询回复等九大功能模块,业务流程完整。压缩包共 1401 个文件,体积 19.39MB,以 Java 后端、Vue 管理端、小程序 wxml/wxss 页面及数据库脚本为主,同时包含设计文档与部署配置,导入 mysql 后即可快速运行,目录结构清晰,便于按模块学习。目前已有 54 人学习浏览。对需要快速搭建农产品电商小程序原型、理解前后端分离开发与小程序发布流程的读者来说,这份源码既可作毕设框架,也能用于课程项目演练。
1. 农产品自主供销小程序源码:能直接交毕设的完整工程
每年毕业季都有人拿着“基于小程序的农产品自主供销小程序源码”来找我帮看,说怕下载下来跑不起来。这套资源包含 Java 后端、微信小程序前端和配套的毕业设计 LW 文档,是计算机毕业设计或课程设计场景下非常典型的一类完整工程。它不是网上那种只有首页和登录页的残次品,而是覆盖了农户发布农产品、买家发布需求、系统按分类和地区做双向匹配、订单状态流转、个人中心管理这一整条业务闭环。适合正在赶毕设需要快速跑通演示的学生,也适合刚学 Spring Boot 想找完整项目练手的人。下面我按自己拆项目的习惯,从技术栈、表结构、核心逻辑、部署排错到加分优化,一层一层说清楚。
2. 技术栈与系统设计:先理清 Spring Boot 和小程序的分工
2.1 技术选型:为什么是 Spring Boot 加微信小程序原生框架
小程序类毕业设计的主流做法是前后端分离,前端跑在微信开发者工具里,后端是独立的 Java 服务。这套源码的后端用 Spring Boot,前端用微信小程序原生框架,数据库用 MySQL。选 Spring Boot 而不是 SSM,理由很现实:第一,内嵌 Tomcat,不用单独装外部服务器部署 war 包,IDEA 里直接运行 main 方法服务就起来了;第二,配置集中在 application.yml 一个文件里,数据源、端口、上传大小、日志级别一眼扫完;第三,启动错误堆栈定位精准,对新手排错非常友好。
用 SSM 的话,需要维护 web.xml、spring-mvc.xml、mybatis-config.xml 好几个配置文件,一个地方写错,报错信息往往和真正原因对不上,排查成本高出一截。我给学生的建议是,除非学校强制要求 SSM,否则 Spring Boot 是更稳的选择,尤其是答辩时讲注解、自动配置、starter 这些点,比讲 XML 配置好展开得多。
| 对比项 | Spring Boot | SSM(Spring+SpringMVC+MyBatis) |
|---|---|---|
| 启动方式 | 内置 Tomcat,直接运行 main 方法 | 外置 Tomcat,打 war 包部署 |
| 配置复杂度 | application.yml 覆盖大多数配置 | 多个 XML 互相引用 |
| 排错体验 | 堆栈精准,日志直观 | 配置问题层层绕 |
| 答辩展开度 | 注解、自动配置、starter 都好讲 | 绕不开 XML 手动装配 |
小程序端选择原生框架而不是 uni-app,原因也不复杂。原生 WXML/WXSS/JS 没有跨端编译层,接口调用最直接,断点调试最直观。毕设里用到的基本就是页面跳转、网络请求、本地缓存、表单校验这些基础能力,原生完全够用。等以后真要跑抖音小程序或者支付宝小程序,再考虑 uni-app 那套跨端方案,不是一个赛道的事。
2.2 数据库设计:用户、商品、供销需求与订单四张核心表
拿到源码第一件事我会先看 SQL 脚本,整个系统的业务逻辑基本都体现在表结构里。这套设计里最核心的是四张表:用户表、商品表、供销需求表、订单表。用户表里除了常规的 id、username、password,关键字段是 role 和 region,role 区分普通买家和农户两种角色,region 记录地区,是后面匹配逻辑的地基。商品表挂在农户登录态下,每条记录带 seller_id、category、price、unit、stock、region,这些字段几乎全是为供销匹配服务的。
供销需求表是这个项目“自主供销”概念的落点:买家可以主动发布想买的农产品,农户可以发布供应的农产品,系统按分类和地区做双向聚合。订单表记录交易过程,状态字段至少包含待付款、待发货、待收货、已完成、已取消五个状态,每次状态变更都要写库,不允许前端只改页面临时状态。
CREATE DATABASE IF NOT EXISTS farm_supply DEFAULT CHARACTER SET utf8mb4; USE farm_supply; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, role TINYINT NOT NULL DEFAULT 0 COMMENT '0-普通用户 1-农户', phone VARCHAR(20), region VARCHAR(50), avatar VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, seller_id INT NOT NULL, name VARCHAR(100) NOT NULL, category VARCHAR(50) NOT NULL COMMENT '蔬菜/水果/粮油/禽蛋', price DECIMAL(10,2) NOT NULL, unit VARCHAR(10) DEFAULT '斤', stock INT DEFAULT 0, description VARCHAR(500), image VARCHAR(255), region VARCHAR(50), status TINYINT DEFAULT 1 COMMENT '1-上架 0-下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (seller_id) REFERENCES user(id) ); CREATE TABLE supply_demand ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, category VARCHAR(50) NOT NULL, product_name VARCHAR(100), quantity INT, region VARCHAR(50), demand_type TINYINT COMMENT '1-供应 2-需求', status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, buyer_id INT NOT NULL, seller_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0-待付款 1-待发货 2-待收货 3-已完成 4-已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (buyer_id) REFERENCES user(id), FOREIGN KEY (seller_id) REFERENCES user(id), FOREIGN KEY (product_id) REFERENCES product(id) );这里有几个参数细节值得记一下。price 用 DECIMAL(10,2) 而不用 FLOAT,农产品单价虽然大多数是个位数,但涉及金额计算就必须保证精度,FLOAT 在算总价时容易出现 0.1 加 0.2 不等于 0.3 的尴尬结果。第二个是显式指定 utf8mb4 字符集,不指定的话存商品描述里的表情符号或生僻字会直接报错,现在建库建表我都会默认带上。第三个是订单表单独存了 buyer_id 和 seller_id,订单创建时就把买卖双方固定下来,后面即使商品记录被下架,订单历史查询也不受影响。
再提醒两个建表时容易翻车的点。表名不要用 order,这是 SQL 的保留字,很多人第一次建订单表卡在这,SQL 直接飘红,所以脚本里写的是 orders。时间字段建议用 DATETIME 而不是 TIMESTAMP,TIMESTAMP 有 2038 年问题并且受时区影响,DATETIME 更稳定。
2.3 后端包结构与小程序端目录对照
后端工程打开后,package 目录一般这样分:controller、service、mapper、entity、config、common。controller 接收 HTTP 请求并调度 service;service 放业务逻辑,供销匹配和订单状态机都在这层;mapper 对应 SQL 操作,通常基于 MyBatis-Plus 的 BaseMapper;entity 映射数据库表,字段名和表字段保持一致;config 里放跨域配置、拦截器、JWT 过滤器;common 放统一返回结果类 Result 和分页封装。
小程序端每个页面一个文件夹,包括 wxml、wxss、js、json 四个文件。首页是 pages/index,商品列表是 pages/goods,发布商品是 pages/publish,订单列表是 pages/order,个人中心是 pages/me。页面跳转通过 wx.navigateTo 或 wx.switchTab,页面间传参靠 url 拼接,这些机制要先搞懂,逛页面代码时才不会被跳转逻辑绕晕。
| 后端包名 | 职责 | 对应小程序端切入点 |
|---|---|---|
| controller | 暴露 HTTP 接口 | pages 下各页面的请求入口 |
| service | 业务逻辑,含匹配和状态机 | 多个页面共同调用 |
| mapper | SQL 操作 | 无直接对应 |
| entity | 表结构映射 | 无 |
| config | 跨域、拦截器、token 校验 | utils/request.js |
| common | 统一返回体和异常 | utils/auth.js |
2.4 毕业设计 LW 文档:跳出源码的答辩材料
这套资源附带 LW 文档,在国内毕设生态里很重要,因为多数学校要求“代码加论文加答辩演示”三件套。LW 文档通常包含绪论、需求分析、系统设计、数据库设计、系统实现、测试、总结这些章节,和毕业论文模板基本对应。但要注意,每个学校对格式要求不一样,字体字号、图表编号、页眉页脚,拿到后先对照自己学校的模板确认一遍再交。
我改这种文档的习惯是,先建一个格式对照表,把自己学校模板的章标题、节标题、图注、表注、参考文献格式列出来,然后逐段替换。文档里需要刻意准备呼源码的素材有四处:数据库 E-R 图、系统架构图、核心代码截图、测试用例表。这些直接从源码里截取就行,重点是让导师看到你确实把系统跑通并理解了项目。
3. 核心业务实现:登录、供销匹配与订单状态机
3.1 小程序端登录与用户授权:code 换 openid 的完整链路
小程序登录和传统 Web 登录不一样,核心是用 wx.login 返回的 code 换 openid。很多第一次写小程序的人会踩一个经典的坑:以为 code 就是用户身份,直接存起来用。实际上 code 有效期只有五分钟,需要用 code 调微信接口换 openid 和 session_key。后端拿到 openid 后把它当成用户唯一标识,查一下用户表,没有就自动注册。因为 openid 不像手机号那样有明文可见的体验,所以登录和注册接口实际上可以合并成一个,加一个角色字段默认普通用户,在个人中心再切换农户身份。
@RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @PostMapping("/login") public Result login(@RequestBody LoginRequest req) { // 1. 调用微信接口,用 code 换 openid String openid = userService.code2Openid(req.getCode()); if (openid == null) { return Result.error("登录凭证已失效,请重试"); } // 2. 查用户是否存在,不存在则自动注册 User user = userService.findByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setRole(0); userService.register(user); } // 3. 签发自定义 token,后续请求带 token 访问 String token = JwtUtil.generateToken(user.getId(), user.getRole()); return Result.success(new LoginResponse(token, user.getId(), user.getRole())); } }这段逻辑里有三个关键点要说。code2Openid 这一步通常在 application.yml 里配置小程序的 AppID 和 AppSecret,如果填成测试号的配置,真机登录时会匹配不上。JwtUtil 生成 token 时把 userId 和 role 塞进 payload,后续 controller 通过拦截器就能拿到当前用户是谁、是买家还是农户,不用每次查数据库。Result 是统一返回体,格式是 code、message、data 三件套,小程序端 request.js 里先判断 code 再继续业务逻辑,这是前后端联调的基本约定。
3.2 供销匹配:不是简单列表而是双向推荐
“自主供销”这四个字,重点在自主匹配。常见做法是用户既能发布供应信息,也能发布需求信息,系统按分类和地区自动做双向推荐。这个逻辑听起来简单,写起来容易翻车的点在匹配规则设计上。毕设场景不建议用太复杂的推荐算法,协同过滤、用户画像这类名词虽然唬人,但老师追问原理讲不清楚反而扣分。用规则匹配就够:先按 category 精确匹配,再叠加 region 条件,最后按 create_time 倒序取最近几条,每一条规则答辩时都能展开讲清楚。
@Service public class SupplyDemandServiceImpl implements SupplyDemandService { @Autowired private SupplyDemandMapper supplyDemandMapper; public List<SupplyDemandVO> matchSupply(DemandQuery query) { // 规则一:按分类匹配,保证供给和需求属于同一品类 List<SupplyDemand> list = supplyDemandMapper.selectByCategory(query.getCategory()); // 规则二:按地区过滤,优先推荐同城农产品 List<SupplyDemand> filtered = list.stream() .filter(item -> query.getRegion().equals(item.getRegion())) .collect(Collectors.toList()); // 规则三:按时间倒序,只取最近发布的 8 条 filtered.sort((a, b) -> b.getCreateTime().compareTo(a.getCreateTime())); return filtered.stream().limit(8).map(SupplyDemandVO::from).collect(Collectors.toList()); } }三个规则对应三种真实用户场景。分类匹配解决的是农产品品类太多、乱推荐没意义的问题;地区过滤解决的是不同城市价格差异大、用户优先看本地的问题;时间倒序解决的是农产品可能已经卖完或过期、优先看最新发布的问题。需要注意的边界情况是,如果同分类下过滤结果不足 8 条,前端要处理空态,显示“暂无匹配供给,试试切换地区或分类”,不能白屏。
我一般会在匹配逻辑里再加一个开关,把地区过滤做成可选的。因为有些农产品是跨区域流通的,比如海南的椰子,本地没供给,需要放宽地区限制才能匹配到。所以查询参数加一个 cityScope 字段,默认 1 表示只查本地,置 0 时忽略地区条件,这样通用性会好很多。
3.3 订单状态机:五个状态之间的合法迁移路径
订单系统最怕乱改状态,所以必须用状态机约束流转方向。这套系统里五个状态的合法迁移路径是:待付款可以到待发货或已取消,待发货可以到待收货,待收货可以到已完成,已完成和已取消是终态。不能用两个 if 直接判断完事,散落的状态判断在后期改需求时会非常痛苦,集中管理才是正确的做法。
public boolean changeOrderStatus(Long orderId, Integer oldStatus, Integer newStatus, Long userId) { Order order = orderMapper.selectById(orderId); if (order == null) { throw new BusinessException("订单不存在"); } // 用 Map 定义合法状态迁移,避免散落一地的 if 判断 Map<Integer, List<Integer>> transferMap = new HashMap<>(); transferMap.put(0, Arrays.asList(1, 4)); transferMap.put(1, Arrays.asList(2)); transferMap.put(2, Arrays.asList(3)); List<Integer> nextStatus = transferMap.get(oldStatus); if (nextStatus == null || !nextStatus.contains(newStatus)) { throw new BusinessException("非法状态迁移"); } // 校验操作人身份:只有买家能付款,只有卖家能发货 if (oldStatus == 0 && newStatus == 1) { if (!order.getBuyerId().equals(userId)) { throw new BusinessException("只有买家可操作付款"); } } if (oldStatus == 1 && newStatus == 2) { if (!order.getSellerId().equals(userId)) { throw new BusinessException("只有卖家可操作发货"); } } orderMapper.updateStatus(orderId, newStatus); return true; }这里有一个很容易忽略的点:oldStatus 必须从数据库实时读取,而不是从前端传。前端页面上显示的状态可能是陈旧数据,如果直接信任前端传过来的状态做判断,并发场景下会出现重复付款或者订单已取消却能发货的情况。所以接口设计上只传 newStatus,旧状态永远以数据库为准。
状态转移 Map 的设计越简单越好。有人会写成十几个 switch case,把判断散落在 service 不同方法里,最后改一个状态要在三个地方同步改。用 Map 集中管理后,新增状态只需要改 transferMap 那一行,排查时也只要看一个数据结构。
4. 本地部署和联调:从 SQL 脚本到小程序能交互动
4.1 后端启动前必做的四步配置
拿到源码第一件事不是点启动,而是把环境配置统一好。JDK 版本、Maven 仓库、MySQL 版本这四项不一致会带来很多奇怪报错。最常见组合是 JDK 1.8 或 JDK 11,Spring Boot 2.x 在这两个版本下兼容性最好。MySQL 用 5.7 或 8.0 都行,但驱动配置写法略有区别。核心配置要改 application.yml,下面这份是常见模板:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/farm_supply?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 你的数据库密码 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl jwt: secret: farm-supply-jwt-secret-key expire-days: 7 wx: appid: 你的小程序AppID secret: 你的小程序AppSecret配置里有三个关注点。url 里的 serverTimezone=Asia/Shanghai 不能省略,没有它 MySQL 8.0 连接时会报时区异常。characterEncoding 建议直接用 utf8mb4,和前面建库时保持一致,避免中文乱码。multipart 大小限制决定农产品图片能传多大,后端不做图片压缩的话,前端压完图后一般控制在 5MB 内就够。
第二步是执行 SQL 脚本建库,第三步在 IDEA 里用 Maven 刷新依赖,第四步启动 Application 主类。启动时观察控制台日志,看到 Tomcat started on port 8080 说明后端起来了。如果日志里报 Failed to configure a DataSource,基本就是 yml 里的数据库地址或密码没对上。
4.2 小程序端导入微信开发者工具的四个关键设置
后端跑起来后,前端才能发起请求。在微信开发者工具里导入项目时,很多人卡在 AppID 上。个人开发者没有企业 AppID,选择测试号是可行的,测试号通过编译后调用的域名不受企业限制,调本地后端接口没问题。
进项目后第一个要改的是 config.js 里的 baseURL。代码里可能是 http://localhost:8080,在开发者工具里能访问本机,但部署到真机时要把 localhost 换成电脑的局域网 IP,手机和电脑要在同一 WiFi 下。
// config.js 环境配置文件 module.exports = { // 开发环境:微信开发者工具可访问 localhost // 真机调试时改成 http://192.168.x.x:8080 baseURL: 'http://localhost:8080', timeout: 10000, }第三个关键设置是“详情”面板里的“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”选项,开发阶段允许小程序请求 HTTP 接口的开关,不勾的话请求会被拦截。第四个设置是清缓存,每次改完代码重新编译,如果数据没有刷新,清一下缓存再编译,能排除旧代码的干扰。
4.3 接口联调:用 Apifox 先自测后端,再跑通小程序端主链路
开发阶段尽量不直接在小程序端调后端,小程序端页面一跳转就很难区分问题是前端还是后端。我一般会先用 Apifox 把后端接口参数和返回结构调通,再切到小程序端做端到端验证。
# 用 curl 快速验证登录接口 curl -X POST http://localhost:8080/api/user/login \ -H "Content-Type: application/json" \ -d '{"code":"test_code_001"}' # 查看商品列表接口 curl "http://localhost:8080/api/product/list?page=1&size=10"如果登录接口返回的 code 是 0 且 data 里有 token,说明后端正常。接下来用 Apifox 走一次下单主链路:登录拿 token,带 token 创建订单,再模拟付款、发货、收货。这条链路能跑通,说明数据库、拦截器、状态机三个关键模块没问题。同时再把 MyBatis 日志打开,后端会打印每次执行的 SQL,配合 Apifox 不用打开数据库就能看到数据写入情况,联调效率会高不少。
4.4 真机调试时的网络配置排查
模拟器上跑通不算真的跑通,真机演示才是毕业设计最容易翻车的一环。真机上最经典的问题就是在 config.js 里留了 localhost,手机上的 localhost 指向手机自己,自然连不上电脑的后端。改局域网 IP 还不行的话,先试试电脑防火墙有没有拦掉 8080 端口,Windows 上经常是防火墙默认拦了 Java 进程。
判断请求到底有没有发出去,常规做法是用 Charles 这类工具抓包看请求包,能明显看到请求 URL、请求头、返回体。如果抓包看到请求已经到后端但返回 404,检查一下 controller 的路径映射;如果请求压根没发出去,基本就是前端 baseURL 或域名校验的问题。
5. 避坑指南:小程序毕业设计最常见的五个坑
5.1 登录提示 code 无效或 openid 为空
现象:小程序端点击登录,后端返回“code 无效”,日志里显示微信接口返回 errcode 40029。
原因:最常见是 AppID 和 AppSecret 配置错了,写成了测试号的对应值;其次是前端拿着同一个 code 重复换两次 openid,微信的 code 只能使用一次。
解决:每次登录都在小程序端重新调用 wx.login 拿新 code,后端只换一次 openid,不要缓存 code。AppSecret 不要硬编码在源码里,至少放到配置文件并从代码中排除提交,答辩时被问到密钥泄露问题时也有话可讲。
5.2 图片上传后小程序端显示不出来
现象:商品发布时图片上传提示成功,但列表页图片破图或灰块。
原因:后端存储图片用的是本地磁盘路径,比如 D:/uploads/a.jpg,小程序端访问不到这个路径,必须转成 http 地址才能加载。
解决:图片访问地址必须拼接成 http://ip:8080/upload/a.jpg 的形式,并在后端配静态资源映射。Spring Boot 里加下面的配置类,把本机 upload 目录映射成可通过 URL 访问的资源:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + System.getProperty("user.dir") + "/upload/"); } }这段配置里file:前缀是关键,必须保留,否则会被当成 classpath 路径处理。System.getProperty("user.dir")获取的是当前工作目录,IDEA 中通常是工程根目录,所以要保证上传目录建在工程根目录下。
5.3 请求报跨域或被拦截
现象:在小程序端发请求,控制台报 invalid url 或无法连接服务器。
原因:小程序端和浏览器跨域机制不一样,小程序不存在严格意义上的浏览器跨域,但开发者工具会模拟部分限制。最常出问题的是真机调试时 baseURL 写成了 localhost,真机上的 localhost 指向手机自己,根本到不了电脑后端。
解决:开发工具里勾选“不校验合法域名”,真机调试时把 baseURL 改成电脑的局域网 IP,并确保电脑防火墙放行 8080 端口。后端再配一层全局 CORS 是合理的保险动作,方便以后万一要接 Web 端管理台时不用再改。
5.4 订单状态卡在待发货
现象:买家付款后,订单一直处于待发货状态,卖家端点击发货没有任何变化。
原因:卖家端页面只改了本地缓存里的订单状态,没有调后端接口,或者调用了但传的参数 newStatus 不对。
解决:检查发货按钮的点击事件是否调用更新状态接口,确认 newStatus 传的是代表已发货的状态值。给按钮加一个日志输出,把当前状态值打出来看,问题往往立刻定位到是前端缓存写入覆盖了接口返回值。
5.5 启动报 Access denied for user
现象:启动后端,第一次请求数据库时报 Access denied for user 'root'@'localhost'。
原因:application.yml 里的密码写错了,或 MySQL 用户权限不足,或 MySQL 8 默认使用 caching_sha2_password 认证插件而数据库驱动版本不支持。
解决:先确认密码正确,再更新 MySQL 连接驱动为最新版,也可以在连接串中加 allowPublicKeyRetrieval=true 和 useSSL=false。JDK 版本、MySQL 认证插件、驱动版本这三个变量在环境不一致时特别容易组合出问题,建议统一用一个 Docker 镜像里的 MySQL 来学习,避免被宿主机环境干扰。
6. 代码能跑只是及格:请求封装与骨架屏两个加分项
6.1 统一请求封装,干掉每页重复的 wx.request
很多毕设代码在每个页面直接写 wx.request,一个问题重复散落在十几个页面里,答辩时老师问“token 过期你在哪里处理”就答不上来。常见做法是封装两层:基础 request 函数统一处理 token 注入、超时、错误码拦截,基于它再生成业务 api 模块,页面只关心业务数据。
// utils/request.js 统一请求封装 const request = (url, method, data) => { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token') wx.request({ url: getApp().globalData.baseURL + url, method, data, header: { 'Content-Type': 'application/json', 'token': token || '', }, success: (res) => { if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data.data) } else if (res.data.code === 401) { // token 过期统一跳登录页 wx.removeStorageSync('token') wx.reLaunch({ url: '/pages/login/login' }) reject(new Error('登录状态已过期')) } else { // 统一弹错误提示,页面不用重复写 toast wx.showToast({ title: res.data.message || '请求失败', icon: 'none' }) reject(new Error(res.data.message)) } }, fail: (err) => { wx.showToast({ title: '网络异常,请检查网络', icon: 'none' }) reject(err) }, }) }) } // api/product.js 页面实际调用的接口 const getProductList = (params) => request('/api/product/list', 'GET', params) const createOrder = (data) => request('/api/order/create', 'POST', data) module.exports = { getProductList, createOrder }经过这样改造,业务页面里不再出现 token 和错误提示逻辑,以后新增接口只要在 api 文件里加一行。更重要的是,答辩时能直接指着代码说“我在请求层统一做了登录态管理和异常拦截”,这是工程化意识的体现。
6.2 商品列表加骨架屏,演示不露怯
答辩现场最怕网速差时商品列表白屏,改起来其实成本很低。在页面的 onLoad 阶段先用一个 loading 状态控制视图,渲染八块灰色占位区域,请求成功后换成真实列表。微信小程序原生的骨架屏也可以用 view 模拟,用几行 wxml 和 wxss 就能实现。
<!-- 商品列表骨架屏示例 --> <view wx:if="{{loading}}" class="skeleton-list"> <view class="skeleton-item" wx:for="{{[1,2,3,4]}}" wx:key="index"> <view class="skeleton-img"></view> <view class="skeleton-line"></view> </view> </view> <view wx:else> <!-- 真实商品列表 --> </view>这样当数据没返回时,用户看到的是稳定的界面框架而不是空白页,体验上会舒服很多。从那以后我每次拿到新项目源码,都会先做一件事:把代码里所有 wx.request 搜出来,看是否都被统一封装接管;再检查登录链路里 token 过期有没有全局处理。两个问题都达标,这份源码才敢直接拿去演示和答辩。希望帮到你。
本文还有配套的精品资源,点击获取