☰
Spring Boot + uniapp校园快递平台:从JWT登录到代取订单全栈实战
2026/9/26 2:07:10 网站建设 项目流程

简介:基于Spring Boot与uniapp的校园快递平台系统完整源码,定位于校园快递服务管理场景,面向在校师生、系统开发者及毕业设计学习者。项目同时提供后台管理端与用户端,功能涵盖用户注册登录、快递寄送接收、快递信息录入与状态查询、饼状图与柱状图统计、数据库备份与导入、系统配置管理以及字典信息管理,覆盖从日常操作到后台维护的完整业务闭环。压缩包内共1502个文件,主要以Java源码、Vue页面、JavaScript逻辑、JSON配置以及SQL数据库脚本组成,同时包含图标、样式表、小程序页面文件、说明文档等多类资源,整体体积24.26MB,结构清晰便于按模块查阅。目前已有33人浏览学习,通过该源码可快速搭建一套可运行的校园快递平台,并深入理解Spring Boot后端接口与uniapp跨端页面间的数据交互方式、统计图表生成逻辑以及工程目录组织技巧。

1. 校园快递平台系统是做什么的:一套代码管住取件、寄件和代取

校园快递最大的痛点不是快递多,而是取件时间永远和上课时间撞在一起。驿站排长队、快递柜不够用、代取靠熟人拼运气。这套基于Spring Boot和uniapp的校园快递平台系统,就是冲着这个场景来的——学生端用uniapp写一套代码,编译成微信小程序或App;后端用Spring Boot提供接口,管用户、快递单、代取订单和通知。系统里包含完整的源码,适合做毕业设计、Java课程设计,或者当作学习Spring Boot全家桶和uniapp跨端开发的练手项目。

这套系统解决的不只是“查快递到没到”,而是把取件流程拆成了三段:快递员入库、学生收到取件码、没空取就发布代取让别人帮你取。这三段各自有状态流转,后端要做权限控制和订单管理,前端要做页面渲染和消息提醒。作为一个全栈项目,它麻雀虽小五脏俱全,能跑通JWT登录、MyBatis-Plus操作数据库、微信小程序登录、订单状态机这些常见面试点。

适合谁?想找Java全栈项目练手的学生,想快速搭一个校园服务类小程序做课程设计的开发者,以及想了解Spring Boot + uniapp如何配合做前后端分离项目的人。下面直接从架构拆解讲起,后面每一步都能照着复现。

2. 后端设计:Spring Boot 分层、JWT 登录与快递单核心表

2.1 Spring Boot 项目分层:为什么 controller 里不能写业务逻辑

拿到这个项目的源码后,第一件事不是急着跑起来,而是先看包结构。一个标准的分层结构长这样:

com.campus.express ├── controller // 接口层:接收请求、返回结果 ├── service // 业务层:写核心逻辑,比如下单、状态流转 ├── mapper // 数据访问层:MyBatis-Plus 的 Mapper 接口 ├── entity // 实体类:对应数据库表 ├── dto // 传输对象:接收前端参数、返回给前端的结构 ├── config // 配置类:JWT 拦截器、跨域、MyBatis-Plus 分页 ├── common // 通用类:统一返回结果、异常处理、工具类 └── controller/admin // 管理员端接口,单独放一个包

controller 层只做三件事:接收参数、调用 service、把 service 返回的结果包装成统一格式。业务逻辑写在哪,决定了你后面调试的时候是改一个文件还是翻五个文件。这个项目里,代取订单的状态流转——待接单、已接单、已取件、已送达——全部放在 service 层的OrderService里,用状态机的方式驱动,而不是在 controller 里写一堆 if/else。

统一返回结果是一个容易被忽略但很重要的设计。这个项目用的结构是:

public class Result<T> { private Integer code; // 200 成功,400 参数错误,401 未登录,500 服务器异常 private String message; // 给前端的提示信息 private T data; // 业务数据 public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("ok"); r.setData(data); return r; } public static <T> Result<T> error(Integer code, String message) { Result<T> r = new Result<>(); r.setCode(code); r.setMessage(message); return r; } }

前端拿到这个结构后,统一在响应拦截器里判断code是不是 200,不是就弹提示。这样设计的好处是,接口报错时前端不看 HTTP 状态码,只看业务码,调试的时候定位问题快得多。

需要解释一个关键参数:code用 200 而不是 0,是因为 HTTP 状态码本身就是 200 表示成功,前后端沟通成本低,前端拦截器里写起来也顺。如果后续要对接更多客户端,业务码的语义比 HTTP 状态码丰富,比如 401 专门表示 token 过期,前端收到后自动跳回登录页。

2.2 JWT 登录鉴权:一个小程序端的 token 处理方案

校园快递平台涉及三种角色:学生、快递员、管理员。快递员入库要登录,学生下单要登录,管理员看统计也要登录。如果用传统的 Session 方案,小程序端每个请求都要带 Cookie,还要处理跨域时的 Cookie 丢失问题,麻烦。这个项目用的是 JWT(JSON Web Token),登录成功后后端签发一个 token 字符串返回给前端,前端存起来,之后的请求在 Header 里带上Authorization: Bearer <token>。

核心代码在登录接口里:

// 登录成功后生成 JWT String token = JwtUtil.createToken(user.getId(), user.getRole()); // JwtUtil 里的核心方法 public static String createToken(Long userId, String role) { return JWT.create() .withClaim("userId", userId) // 自定义声明:用户 ID .withClaim("role", role) // 自定义声明:角色,用于权限判断 .withExpiresAt(new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000)) // 7 天过期 .sign(Algorithm.HMAC256("your-secret-key")); // 签名密钥,生产环境要放到配置中心 } // 解析 token 拿用户信息 public static Long getUserId(String token) { return JWT.require(Algorithm.HMAC256("your-secret-key")) .build() .verify(token) .getClaim("userId").asLong(); }

参数说明里最值得注意的就是过期时间。小程序端的用户不会频繁登录,把过期时间设成 7 天是合理的,但如果做的是快递员端,建议改成 2 小时,因为快递员用的可能是驿站共用的安卓设备,token 过期短一点更安全。密钥不要硬编码在代码里,放到application.yml中用${jwt.secret}引用,后续换密钥不用重新编译。

有了 token 还不够,拦截器必须配上。这个项目里写了一个JwtInterceptor,注册到 Spring MVC 的拦截器链中,拦截所有/api/**请求,放行/api/auth/**登录相关接口。如果 token 校验失败,直接返回 401。

2.3 快递单表设计:用状态字段驱动整个取件流程

快递单是整个系统的核心数据,所有角色都围绕它工作。表结构设计得不好,后面做统计、做通知都会很痛苦。这个项目里快递单表的设计大致如下:

CREATE TABLE `express_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '快递单ID', `express_no` varchar(32) NOT NULL COMMENT '快递单号,即运单号', `student_id` bigint(20) NOT NULL COMMENT '收件学生ID,关联user表', `courier_id` bigint(20) DEFAULT NULL COMMENT '快递员ID,入库时填写', `pickup_code` varchar(8) NOT NULL COMMENT '取件码,例如A-3-102', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待取件 1已取件 2已代取 3超时 4已退回', `shelf_no` varchar(16) DEFAULT NULL COMMENT '货架编号,例如 B区-2层', `arrived_time` datetime DEFAULT NULL COMMENT '入库时间', `pickup_time` datetime DEFAULT NULL COMMENT '取件时间', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_student_status` (`student_id`, `status`), KEY `idx_pickup_code` (`pickup_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='快递单表';

状态字段status是关键。我见过很多课设项目喜欢把状态存成字符串,比如“未取件”“已取件”,看起来直观,但后续统计和筛选的时候非常痛苦。用 tinyint 枚举,配合 MyBatis-Plus 的@EnumValue注解,代码里用枚举类表示,既有可读性又能索引优化。索引设计上,(student_id, status)联合索引用于学生端查询“我的快递列表”,pickup_code索引用于快递员扫码或手动输入取件码时快速定位。

代取订单是独立的表,因为一个快递单可以被多次尝试代取,而且代取订单有自己的状态机:

CREATE TABLE `pickup_task` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `express_id` bigint(20) NOT NULL COMMENT '关联快递单ID', `publisher_id` bigint(20) NOT NULL COMMENT '发布代取的学生ID', `taker_id` bigint(20) DEFAULT NULL COMMENT '接单学生ID,未接单为NULL', `reward` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '悬赏金额,单位元', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待接单 1已接单 2已取件 3已送达 4已取消', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_express_id` (`express_id`), KEY `idx_taker_status` (`taker_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代取订单表';

代取流程里有个容易踩坑的业务点:一个快递单同时只能有一个进行中的代取任务。也就是说,发布代取消单后,其他学生仍然可以接单,但一旦有人接了,任务就锁定。这个锁不是在数据库加悲观锁,而是在 service 里先查询状态再更新,用乐观锁兜底——UPDATE pickup_task SET status = 1 WHERE id = ? AND status = 0。这一步操作返回受影响行数,如果为 0 说明被人抢先了,提示“手慢了,任务已被接走”。

3. 前端实现:用 uni-app 做学生端的取件码和代取流程

3.1 全局请求封装:把 uni.request 包成 Promise,同时处理登录态

uniapp 自带uni.request,但直接在每个页面里写uni.request({...})会让代码膨胀得没法看。这个项目里统一封装了一个request.js,所有页面共用。封装的核心不只是把回调转成 Promise,而是集中处理三件事:token 注入、业务码判断、401 跳转。

// utils/request.js const BASE_URL = 'http://localhost:8080/api'; // 后端接口地址 export function request(options) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': uni.getStorageSync('token') ? 'Bearer ' + uni.getStorageSync('token') : '' }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data); } else if (res.data.code === 401) { // token 过期:清掉本地登录态,跳回登录页 uni.removeStorageSync('token'); uni.removeStorageSync('userInfo'); uni.navigateTo({ url: '/pages/login/login' }); reject(new Error('登录已过期')); } else { uni.showToast({ title: res.data.message, icon: 'none' }); reject(new Error(res.data.message)); } }, fail: (err) => { uni.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); }

这里的BASE_URL是调试阶段的写法。用微信开发者工具调试时,后端跑在本机 8080 端口,小程序端可以直接访问localhost。但真机预览时,手机访问不到电脑的 localhost,得把BASE_URL改成电脑的局域网 IP,例如http://192.168.1.5:8080/api。这个项目在源码里留了注释提醒改这个地址,真机联调时最容易在这里翻车。

登录态处理有个细节值得注意:token 存到uni.getStorageSync还是uni.setStorageSync的 key 名要保持全局统一,否则容易出现存的时候叫token,取的时候写成了userToken,结果每次请求都 401 的情况。

3.2 取件码查询页面:列表渲染与下拉刷新

学生最常用的页面就是首页的快递列表,进小程序先看有没有自己的快递,看到取件码后直接去驿站取件。列表页用onShow生命周期而不是onLoad,因为学生可能从代取详情页返回列表页时,快递状态已经变了,需要重新拉取数据。

<template> <view class="container"> <view v-for="item in expressList" :key="item.id" class="express-card"> <view class="express-header"> <text class="status">{{ statusText(item.status) }}</text> <text class="pickup-code" v-if="item.status === 0">取件码:{{ item.pickupCode }}</text> </view> <view class="express-body"> <text>快递单号:{{ item.expressNo }}</text> <text>入库时间:{{ formatTime(item.arrivedTime) }}</text> <text>货架位置:{{ item.shelfNo }}</text> </view> </view> <view v-if="expressList.length === 0" class="empty"> <text>暂无快递,去逛逛吧</text> </view> </view> </template> <script> import { request } from '../../utils/request.js'; export default { data() { return { expressList: [], page: 1, hasMore: true }; }, onShow() { this.loadExpressList(true); }, onPullDownRefresh() { this.loadExpressList(true).finally(() => { uni.stopPullDownRefresh(); }); }, methods: { async loadExpressList(reload = false) { if (reload) { this.page = 1; this.hasMore = true; } if (!this.hasMore) return; const data = await request({ url: '/express/my-list', data: { page: this.page, size: 10 } }); if (reload) { this.expressList = data.records; } else { this.expressList = this.expressList.concat(data.records); } this.hasMore = data.records.length === 10; // 单页 10 条,不足说明无更多 this.page += 1; }, statusText(status) { const map = { 0: '待取件', 1: '已取件', 2: '已代取', 3: '超时', 4: '已退回' }; return map[status] || '未知'; }, formatTime(time) { if (!time) return '-'; // 后端返回的是 UTC 时间戳或 datetime 字符串,这里做本地格式化 return time.replace('T', ' ').substring(0, 16); } } }; </script>

分页参数是重点。page从 1 开始,size固定为 10,后端用 MyBatis-Plus 的Page对象返回records数组。当下拉触底加载更多时,用concat追加而不是覆盖,否则会出现列表闪跳。后端返回的时间字段如果是带T的 ISO 格式,前端直接replace('T', ' ')就能显示成平常看到的样式。

这里有个常见的分页陷阱:hasMore的判断不能只看data.records.length === 10,如果恰好最后一页正好 10 条,页面会多触发一次空请求。但作为课设项目,这个判断已经够用,优化方案是让后端返回total字段,前端用page * size < total判断是否还有更多。

3.3 发布代取:表单校验与按钮防重复点击

发布代取页面的核心逻辑在提交按钮上。学生从快递列表点进详情,看到“我取不了,找人代取”按钮,填写悬赏金额和备注,点提交。这里最容易出问题的是按钮重复点击——用户手抖点了两次,后端就生成了两条代取任务。

解决方案分前端和后端两层。前端用submitting标志位控制按钮状态:

async submitTask() { if (this.submitting) return; // 防重复提交 if (!this.selectedExpressId) { uni.showToast({ title: '请选择快递', icon: 'none' }); return; } if (this.reward <= 0) { uni.showToast({ title: '请填写悬赏金额', icon: 'none' }); return; } this.submitting = true; try { await request({ url: '/pickup/create', method: 'POST', data: { expressId: this.selectedExpressId, reward: this.reward, remark: this.remark } }); uni.showToast({ title: '发布成功', icon: 'success' }); setTimeout(() => uni.navigateBack(), 1500); } finally { this.submitting = false; } }

submitting标志位在finally里复位,保证无论成功还是失败,按钮都能恢复点击。只在前端做防重还不保险,真正拦住并发的是后端那次乐观锁UPDATE ... WHERE status = 0,但这已经是上一章讲的内容了。前端这个标志位的作用是拦截掉 99% 的重复点击场景,给后端减压。

3.4 快递员入库页面:扫码录入与手动录入双通道

快递员端在 uniapp 里单独做了一套页面,和学生的入口分开。入库支持微信扫码和手动输入两种方式,扫码用的uni.scanCode拿到快递运单号,然后带入表单,快递员选择货架号、填写收件学生手机号,后端根据手机号匹配用户,生成取件码。

取件码生成规则是这个项目的亮点之一。生成规则是:货架区号 + 层号 + 序号,比如A-2-15表示 A 区第二层第 15 格。后端生成取件码的代码如下:

public synchronized String generatePickupCode(String shelfNo) { // shelfNo 格式如 "A-2",取件码形如 A-2-15 Integer nextSeq = redisTemplate.opsForValue().increment("pickup:seq:" + shelfNo, 1); // 同一货架每天从 1 开始计数 redisTemplate.expire("pickup:seq:" + shelfNo, 24, TimeUnit.HOURS); return shelfNo + "-" + String.format("%03d", nextSeq); }

用 Redis 的自增计数器生成序号,比查数据库MAX(id)+1可靠得多,也顺便练习了 Redis 的使用。不过要注意,这个项目默认的生成规则是按货架维度自增,如果你希望取件码全局唯一,把 key 改成pickup:seq:global就行。用 Redis 的前提是后端已经配置了 Redis 连接,如果源码里没有 Redis 这段,可以退化成数据库查重。

4. 跑通最小闭环:数据库初始化、接口联调与小程序真机预览

4.1 后端启动三步:建库、改配置、跑起来

拿到源码后,最快跑通后端的方法是按这三步走。

第一步是创建数据库并导入初始化脚本。源码包里通常带有sql/init.sql,里面包含建表语句和测试数据。导入后确认表数量和预期一致:

mysql -u root -p < init.sql

第二步是修改application.yml里的连接配置。重点是数据库地址、账号密码,以及 Redis 地址(如果项目用到的话):

spring: datasource: url: jdbc:mysql://localhost:3306/campus_express?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password redis: host: localhost port: 6379 mybatis-plus: global-config: db-config: id-type: auto configuration: map-underscore-to-camel-case: true

serverTimezone=Asia/Shanghai是必填的,不填的话 JDBC 驱动会用默认时区,通常比东八区慢 8 小时,直接导致入库时间和取件时间对不上。map-underscore-to-camel-case开启后,数据库的pickup_code字段自动映射到实体类的pickupCode,不用手写一堆@TableField注解。

第三步是启动 Spring Boot 应用。用 IDEA 打开项目,等待 Maven 依赖下载完毕,直接运行主类CampusExpressApplication。控制台出现Started CampusExpressApplication就说明启动成功。

验证后端是否健康,不用等前端,直接用浏览器或 Postman 调一个接口:

curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"student01","password":"123456"}'

如果返回结果里有token字段,说明数据库连接正常、登录逻辑正常、JWT 生成正常。这一步是联调前的冒烟测试,能省下大把排错时间。

4.2 uniapp 前端导入与运行:HBuilderX 的配置细节

前端用 HBuilderX 打开源码中的uniapp目录。打开后先确认三件事:项目识别是否正常、manifest.json 里的 appid 是否为空、依赖是否装好。

运行到微信开发者工具需要走以下步骤:HBuilderX 菜单栏点击“运行 -> 运行到小程序模拟器 -> 微信开发者工具”。首次运行会要求填写微信开发者工具的安装路径,并在微信开发者工具里开启服务端口。如果运行后页面白屏,先打开 HBuilderX 控制台看编译报错,再看微信开发者工具的“调试器”里的 Console,两个地方各看一遍基本能找到问题。

小程序端有一个关键配置容易忽略:在微信开发者工具右上角的“详情 -> 本地设置”里勾选“不校验合法域名”。开发阶段后端跑在本地 HTTP 接口上,不勾选这个选项,所有请求都会被微信拦截,报url not in domain list。这个选项只影响开发环境,上线时必须关闭。

4.3 真机预览:从 localhost 到局域网 IP 的切换

电脑上的开发者工具跑通了,手机预览就不得不改地址。手机通过局域网访问电脑的时候,localhost指的是手机自己,不是电脑。这里给出完整的切换流程:

  1. 查看电脑局域网 IP:Windows 用ipconfig,Mac 用ifconfig | grep inet,找到形如192.168.x.x的地址。
  2. 修改utils/request.js里的BASE_URL,把localhost换成该 IP。
  3. 电脑防火墙放行 8080 端口。Windows 上需要在“高级安全 Windows Defender 防火墙”里添加入站规则,否则手机请求会超时。
  4. 手机和电脑连同一个 Wi-Fi,HBuilderX 里点击“运行到手机或模拟器”。

真机预览时最常见的错误是请求超时而不是请求被拒。超时大概率是防火墙拦了端口,被拒通常是后端接口路径写错或 CORS 没配好。

跨域问题的表现是“请求已发送但 response 被浏览器拦截”。虽然小程序端不是浏览器,但在 H5 端调试时跨域就会出现。后端加一个全局跨域配置是最省事的方案:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

allowCredentials(true)和allowedOriginPatterns("*")这个组合要注意:前者允许携带 Cookie 和 Authorization 头,后者用通配符匹配来源。如果用allowedOrigins("*"),会和allowCredentials(true)冲突导致启动报错,换成allowedOriginPatterns就没有这个问题。

5. 常见问题与避坑记录:数据库、跨域、打包的三类典型翻车

5.1 快递状态一直变不过来,缓存和数据库时间不一致

现象:快递员入库后,学生端首页刷新后仍然看不到新快递;或者代取订单状态显示“待接单”,但实际上已经被别人接了。

原因:常见的有两种。第一种是后端用了 Redis 缓存快递列表,快递员入库后没有清理缓存,学生端读的是旧缓存;第二种是前端列表页用了onLoad而没有用onShow,导致页面从后台切回前台时不会重新拉数据。

解决:后端在快递员入库的 service 方法里,主动删除学生端列表对应的缓存 key:

// 入库后清理缓存,key 由 “express:list:” + userId 拼接 redisTemplate.delete("express:list:" + studentId);

前端把onLoad改成onShow,每次页面显示都重新请求接口。如果是暂停在微信小程序后台再切回来,onShow一定会触发,这是小程序生命周期的机制。

这种问题的排查思路是:先分清缓存没失效还是接口没调用。在微信开发者工具里打开 Network 面板,看页面切换时有没有发起新的请求。有请求但数据没变,就是后端缓存问题;没有请求,就是前端生命周期问题。定位到具体层再动手,不要两头瞎改。

5.2 微信小程序里请求报“不在以下 request 合法域名列表中”

现象:在微信开发者工具里预览,控制台报url not in domain list,所有接口都请求失败。

原因:微信小程序对uni.request的请求域名有白名单限制,只有 HTTPS 且在微信公众平台后台配置过的域名才能通过。开发阶段后端跑在http://localhost:8080,既不是 HTTPS,也不在白名单里,自然被拦截。

解决:开发阶段在微信开发者工具的“详情 -> 本地设置”里勾选“不校验合法域名”。注意这一步只对当前项目开发者工具生效,换一台电脑或换个人调试,都要重新勾选一次。上线阶段必须申请 HTTPS 域名并在微信公众平台配置白名单,否则正式版小程序直接废掉。

还有一个隐藏点:如果后端接口是http://192.168.x.x:8080,勾选了不校验合法域名也不代表能通。因为小程序对 IP 地址的访问有独立限制,部分基础库版本会拦截对局域网 IP 的请求。遇到这种情况,把接口地址改回localhost在开发者工具里调试,真机预览时再换 IP,不要在一个环境里混用。

5.3 数据库自增主键传到前端后精度丢失

现象:学生发布代取任务时总是失败,后端日志里显示“expressId 不存在”。学生查看快递列表点“找人代取”时,传过去的快递 ID 变成了131434849539866624之类的长数字,而且以0结尾。

原因:这是经典的 Long 精度丢失问题。数据库bigint类型的自增主键是 19 位数字,微信小程序端 JavaScript 的 Number 类型只能精确表示 2 的 53 次方以内的整数,超过这个范围后最后几位会被四舍五入成 0。前端拿的不是真实 ID,传给后端当然查不到记录。

解决:后端接口返回给前端时,把所有 Long 类型的主键转成字符串:

public class ExpressOrderVO { @JsonSerialize(using = ToStringSerializer.class) private Long id; @JsonSerialize(using = ToStringSerializer.class) private Long studentId; // 其他字段 }

每一条可能传给前端的 ID 字段都要加@JsonSerialize(using = ToStringSerializer.class)。只改实体类不够,因为返回前端的通常是 VO 对象,实体类上的注解不会自动带到 VO 里。排查的方法是全局搜索Long id字段,凡是会出现在接口返回结构里的,统一加注解。

这种问题在本地开发时不容易发现,因为本地生成的 ID 小,没到精度上限。一旦部署到测试环境,数据量大了,ID 超过 2 的 53 次方就突然爆雷。做课设时用本地小数据量碰不上,但有面试问到过“后端 Long 传给前端为什么会丢精度”这类题,能讲清楚原理是加分项。

5.4 uniapp 打包安卓时图标和启动图总是显示默认的

现象:用 HBuilderX 打包成 Android APK 后,安装到手机上,应用图标是 HBuilder 的默认 logo,启动页也是一片白或默认背景。

原因:打包时的图标配置没有在manifest.json里设置完整。uniapp 要求在 manifest.json 的可视化界面里分别配置“应用图标”和“启动图”,并且每种分辨率都要上传一张图。很多人只改了应用图标,启动图没改,打包的时候启动图就用了默认资源。

解决:在 HBuilderX 里打开 manifest.json,切到“App 图标配置”页签,至少把 48x48、72x72、96x96、144x144 这几个常用尺寸的图标补齐。启动图部分选择“自动生成”或将背景色和图像配置好,然后重新打包。打包的时候注意勾选“使用云端打包”,本地打包需要安装 Android Studio 和 SDK,配置复杂,不建议课设阶段折腾。

6. 收尾技巧:给系统加一套私有缓存和定时任务,让它经得起追问

项目跑通之后,别急着交,有两个地方值得再花一个晚上打磨:一是给取件码查询接口加缓存,二是给超时未取的快递加一个定时任务。这两个点做完,不管是答辩还是面试聊项目,你都有东西可以展开说,而不是被问到底就“这个我还没做”。

取件码查询接口可以这样加缓存。学生端首页的快递列表每次刷新都打数据库,尽管 MyBatis-Plus 做了分页查询,但重复查询同一个学生的列表没有意义。用 Spring Cache 或手动 Redis 都能做,手动的方式更直观:

@Service public class ExpressServiceImpl implements ExpressService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private ExpressOrderMapper expressOrderMapper; // 查询学生快递列表,先查缓存,缓存未命中再查数据库 public List<ExpressOrderVO> getStudentExpressList(Long studentId) { String key = "express:list:" + studentId; String cached = redisTemplate.opsForValue().get(key); if (cached != null) { return JSON.parseArray(cached, ExpressOrderVO.class); } List<ExpressOrderVO> list = expressOrderMapper.selectListByStudentId(studentId); // 快递到达和状态变更时清理这个 key redisTemplate.opsForValue().set(key, JSON.toJSONString(list), 5, TimeUnit.MINUTES); return list; } }

缓存时间保留字段说明:5 分钟的过期时间比较合理,太短没有缓存意义,太长又可能让学生看到过期的状态。快递状态流转的接口里,记得删掉对应的 key,保证状态变更后第一时间生效。

超时未取的处理逻辑也很简单。快递入库后 48 小时未取,状态从“待取件”变成“超时”。实务上可以用@Scheduled定时任务扫描:

// 每小时执行一次,把超过48小时未取件且状态仍为0的快递改为超时 @Scheduled(cron = "0 0 * * * ?") public void processTimeoutExpress() { List<ExpressOrder> list = expressOrderMapper.selectTimeoutOrders(48); for (ExpressOrder order : list) { order.setStatus(3); // 3 代表超时 expressOrderMapper.updateById(order); // 同步清理缓存,否则学生端会一直看到待取件 redisTemplate.delete("express:list:" + order.getStudentId()); } }

这套定时任务的场景很适合在答辩时展开,“超时退回首尾该怎么设计”“超时时间能不能做成可配置”,都是加分的讨论点。

我自己的习惯是,课设项目不追求功能多,但追求每个功能都能讲出“为什么这么做”。上面的 Redis 缓存和定时任务,即便源码里没有预设,也值得自己加上去——因为这是面试官区分“调包侠”和“动手党”的试金石。希望这些调试经验和优化思路帮到你,让你拿到这套源码后,不只是让它跑起来,还能改出自己的版本。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询