ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot + uniapp校园快递平台:从JWT登录到代取订单全栈实战

Spring Boot + uniapp校园快递平台:从JWT登录到代取订单全栈实战 简介基于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 ResultT { private Integer code; // 200 成功400 参数错误401 未登录500 服务器异常 private String message; // 给前端的提示信息 private T data; // 业务数据 public static T ResultT success(T data) { ResultT r new Result(); r.setCode(200); r.setMessage(ok); r.setData(data); return r; } public static T ResultT error(Integer code, String message) { ResultT 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 丢失问题麻烦。这个项目用的是 JWTJSON 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 classcontainer view v-foritem in expressList :keyitem.id classexpress-card view classexpress-header text classstatus{{ statusText(item.status) }}/text text classpickup-code v-ifitem.status 0取件码{{ item.pickupCode }}/text /view view classexpress-body text快递单号{{ item.expressNo }}/text text入库时间{{ formatTime(item.arrivedTime) }}/text text货架位置{{ item.shelfNo }}/text /view /view view v-ifexpressList.length 0 classempty 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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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: trueserverTimezoneAsia/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指的是手机自己不是电脑。这里给出完整的切换流程查看电脑局域网 IPWindows 用ipconfigMac 用ifconfig | grep inet找到形如192.168.x.x的地址。修改utils/request.js里的BASE_URL把localhost换成该 IP。电脑防火墙放行 8080 端口。Windows 上需要在“高级安全 Windows Defender 防火墙”里添加入站规则否则手机请求会超时。手机和电脑连同一个 Wi-FiHBuilderX 里点击“运行到手机或模拟器”。真机预览时最常见的错误是请求超时而不是请求被拒。超时大概率是防火墙拦了端口被拒通常是后端接口路径写错或 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 ListExpressOrderVO getStudentExpressList(Long studentId) { String key express:list: studentId; String cached redisTemplate.opsForValue().get(key); if (cached ! null) { return JSON.parseArray(cached, ExpressOrderVO.class); } ListExpressOrderVO 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() { ListExpressOrder list expressOrderMapper.selectTimeoutOrders(48); for (ExpressOrder order : list) { order.setStatus(3); // 3 代表超时 expressOrderMapper.updateById(order); // 同步清理缓存否则学生端会一直看到待取件 redisTemplate.delete(express:list: order.getStudentId()); } }这套定时任务的场景很适合在答辩时展开“超时退回首尾该怎么设计”“超时时间能不能做成可配置”都是加分的讨论点。我自己的习惯是课设项目不追求功能多但追求每个功能都能讲出“为什么这么做”。上面的 Redis 缓存和定时任务即便源码里没有预设也值得自己加上去——因为这是面试官区分“调包侠”和“动手党”的试金石。希望这些调试经验和优化思路帮到你让你拿到这套源码后不只是让它跑起来还能改出自己的版本。本文还有配套的精品资源点击获取
返回列表