校园快递代拿系统JavaWeb课设:订单状态机与并发控制实战

发布时间:2026/10/11 21:40:49
校园快递代拿系统JavaWeb课设:订单状态机与并发控制实战 简介校园快递代拿系统是一套面向高校快递服务场景的JavaWeb项目基于Eclipse与MySQL开发主要服务学生用户和后台管理员解决校园内快递代拿流程中申请、查询和订单管理等问题可快速用于课程设计或项目参考。资源压缩包共117个文件包含19个java源文件、19个jsp页面、19个class编译文件并配有css样式、png图标、html静态页等前端资源整体大小约1.63MB目录结构清晰便于按控制层、业务层与视图层逐模块阅读和调试。目前已有4726人学习下载适合正在做JavaWeb课程设计或毕业设计的初学者也适合需要快速搭建校园服务类管理系统的开发者参考。需注意描述中提示数据库脚本未随包提供读者需自行创建MySQL表结构并配置连接参数才能运行同时可借鉴其MVC分层与AJAX无刷新交互的实现方式为后续二次开发提供便利。1. 校园快递代拿系统一个能跑通“发布-接单-送达”全流程的 Java 课设校园里取快递最烦的不是路远而是驿站下班比人早、快递柜满了就往地上堆。一套基于 Eclipse MySQL 的校园快递代拿系统把“代拿需求发布、抢单、确认送达、互评”做成一条完整业务闭环正是这类问题的标准解法。它打包了用户端、接单端、管理后台三套界面核心是订单状态机与并发控制。适合正在选课设题目、想复现一个“能演示、能答辩、能扩展”的 JavaWeb 项目的人也适合想搞懂 MVC JDBC MySQL 到底怎么串起来的新手。2. 先把业务定成表结构数据库设计是这个系统的地基在写任何 Java 代码之前我最先把角色和订单流程固定下来。这套系统表面上是“发单、接单、确认送达”但落到表结构上如果不提前想清楚状态怎么流转、字段怎么命名后面写 Service 层时会反复改 SQL改到怀疑人生。2.1 角色模型为什么用户表里放 role 字段而不是单独建管理员表系统里有三类角色发起代拿的学生、愿意跑腿的接单人、管理整个订单后台的管理员。最常见的错误是设计三张表学生表、接单员表、管理员表。我一开始也这么干过结果发现一个用户既可能发单也会接单同一个人在多张表里重复出现登录时要查三次表订单表的外键也要对应三次纯属给自己挖坑。正确做法是在用户表里放一个 role 字段1 代表普通用户2 代表管理员。登录后从 Session 里取出当前用户的 role控制菜单显示入口。普通用户既能发单也能接单不需要区分两套账号只是同一个订单不能既是你发又由你接这个约束放在业务层校验。用户表字段我最终定成登录名、密码、盐、昵称、手机号、角色、注册时间。你可能会问盐为什么要单独一个字段因为 MD5 加盐后的密文并不能还原出盐登录时必须从库里把盐取出来重新计算比对所以盐必须存。有的方案把盐和密文拼成一个字段也不是不行但分开存更直观排查问题时一眼就能看出哪个字段是什么。2.2 订单表设计六个状态与字段取舍的完整建表脚本订单是整张表的中心。我拆出六个状态待接单、已接单、配送中、已送达、已完成、已取消。其中“已送达”和“已完成”有区别前者是接单人点送达、等待发起人确认后者是发起人确认收货订单真正关闭。多拆这一步是为了给“超时未确认自动完成”这类后续逻辑留操作空间。我使用 InnoDB 引擎字符集统一 utf8mb4。下面这份 SQL 是可直接执行的建库建表脚本CREATE DATABASE IF NOT EXISTS express_db DEFAULT CHARACTER SET utf8mb4; CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(32) NOT NULL COMMENT 登录名, password varchar(64) NOT NULL COMMENT 加盐MD5后的密文, salt varchar(16) NOT NULL COMMENT 随机盐, nickname varchar(32) DEFAULT NULL, phone varchar(20) DEFAULT NULL, role tinyint(4) NOT NULL DEFAULT 1 COMMENT 1普通用户 2管理员, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE express_order ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 展示给用户的单号, owner_id int(11) NOT NULL COMMENT 发起人ID, taker_id int(11) DEFAULT NULL COMMENT 接单人ID未接单为NULL, pickup_code varchar(16) DEFAULT NULL COMMENT 驿站取件码, pickup_address varchar(128) DEFAULT NULL COMMENT 取件位置如西区驿站3号柜, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待接单 1已接单 2配送中 3已送达 4已完成 5已取消, reward decimal(6,2) NOT NULL DEFAULT 0.00 COMMENT 代拿费用, remark varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status (status), KEY idx_owner (owner_id), KEY idx_taker (taker_id), CONSTRAINT fk_order_owner FOREIGN KEY (owner_id) REFERENCES user (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里逐条说明设计取舍。order_no 是给用户看的单号不用自增 id因为自增 id 很容易暴露业务量我用时间戳加随机数生成即使订单删除也不会复用。pickup_code 和 pickup_address 放在订单表里而不是单独建快递档案表原因很简单这种代拿系统不需要管理驿站库存发起人手动填取件码就够用。reward 用 decimal(6,2) 不用 float。之前有人用 float 存金额页面显示 2.0库里实际是 2.00000001答辩演示时当场翻车。decimal 精确到分这个问题不会出现。status 字段用 tinyint 加注释状态码与含义的对应关系可以整理成一张表状态码含义谁可以触发0待接单发起人可取消1已接单接单人可改为配送中2配送中接单人可改为已送达3已送达发起人可确认完成4已完成终态5已取消终态索引方面待接单列表页的查询条件是 status0 ORDER BY create_time DESC所以 idx_status 必须建用户查自己发布过的订单时条件是 owner_id所以 idx_owner 也建上idx_taker 用于接单人查询接过的订单频率低一些但建上不亏。三个索引都是单列索引不要图省事用一个联合索引覆盖它们联合索引有最左前缀规则反而会让后两个查询失效。外键我只建了 owner_id 到 user.id。taker_id 故意不建外键因为它允许为空而且接单是高频更新操作外键每次更新都做一次约束检查属于纯损耗。这个区别能体现你分得清“强一致”和“弱一致”的位置。2.3 事务边界接单操作为什么要包在事务里新手版接单逻辑一般是两条 SQL 分开执行更新状态是一条写入接单人 id 是另一条。单独执行都没问题一旦中间业务逻辑抛异常就会出现状态变成已接单但 taker_id 还是 NULL 的残缺数据。正确做法是用同一个 Connection 开启事务让两条 SQL 要么都成功要么都回滚。第三章的接单模块会展开完整代码这里先记住一条原则任何涉及“条件更新 关联字段更新”的操作都要放进事务连接不要用默认的自动提交模式。3. 把流程跑通登录鉴权、发单接单与分页查询的实现数据库层定完剩下就是攒业务代码。这一章讲登录、发单、接单和列表查询四个功能覆盖了系统绝大部分交互逻辑。3.1 登录与注册从明文密码到加盐 MD5注册流程是前端提交用户名、密码、昵称、手机号Servlet 层生成随机盐调用 md5WithSalt 算出密文把用户名、密文、盐一起插入 user 表。这里有个细节盐的长度和字符集要提前定死我用 8 位随机字符串取自大小写字母和数字保证落在 varchar(16) 范围内public static String generateSalt() { String chars abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789; Random r new Random(); StringBuilder sb new StringBuilder(8); for (int i 0; i 8; i) { sb.append(chars.charAt(r.nextInt(chars.length()))); } return sb.toString(); } public static String md5WithSalt(String password, String salt) { String input salt password; StringBuilder sb new StringBuilder(); try { MessageDigest md MessageDigest.getInstance(MD5); byte[] result md.digest(input.getBytes(StandardCharsets.UTF_8)); for (byte b : result) { sb.append(String.format(%02x, b)); } } catch (NoSuchAlgorithmException e) { // 实际项目中应记录日志这里演示省略 } return sb.toString(); }核心逻辑就是 input salt password 这一行。加盐的作用是防止两个密码相同的用户产生相同密文也防止预计算好的彩虹表直接反查明文。登录时按用户名查到 salt 和 password再用同一条规则重新计算比对两个密文字符串。登录成功后Session 里这样存HttpSession session request.getSession(); session.setAttribute(userId, user.getId()); session.setAttribute(role, user.getRole());页面根据 role 的值决定显示哪些菜单。普通用户看到“发布订单”“我的发布”“我的接单”管理员额外看到“全部订单”入口。这里要注意 Session 的失效时间我一般设置 30 分钟超过后用户操作时需要重新登录避免长时间挂机被别人借用身份操作订单。3.2 发单与接单状态机驱动的核心流程发单本质是往 express_order 插入一条记录但 insert 前要做字段校验取件码不能为空、reward 不能为负数。这些校验放在 Servlet 层数据库的 NOT NULL 约束只是底线。如果让数据库报错用户会直接看到一段英文堆栈体验很差。接单是系统里最容易出问题的点。完整代码如下public boolean takeOrder(int orderId, int takerId) { String sql UPDATE express_order SET taker_id?, status1, update_timeNOW() WHERE id? AND status0; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, takerId); ps.setInt(2, orderId); return ps.executeUpdate() 1; } catch (SQLException e) { e.printStackTrace(); return false; } }注意 WHERE 子句带了 status0。这条 update 在并发情况下是原子操作两个人同时执行时行锁会让后执行的人 WHERE 条件匹配不到行受影响行数为 0于是第二个请求收到“订单已被接走”的提示。用受影响行数判断成功与否而不是先查再改这是整套代码的关键。那事务在哪里起作用单条 UPDATE 本身就是隐式事务所以这个场景不手动开事务也可以。但如果接单成功后还要插入一条通知记录就得把两步包进同一个事务用同一个 Connection 执行conn.setAutoCommit(false); try { // 执行 status0 条件的 update // 执行通知记录 insert conn.commit(); } catch (SQLException e) { conn.rollback(); }配送中、已送达、已完成三个状态切换复用同一个模板更新条件带上当前状态转移后更新 update_time每次操作都从当前状态步进到下一状态不能跳变。3.3 分页查询与模糊搜索列表页的实用性优化待接单列表不能一次查全部数据一多页面就卡。标准做法是 count 查询和 data 查询分开执行public ListExpressOrder listOrders(int pageNum, int pageSize, String keyword) { String where WHERE 11 ; if (keyword ! null !keyword.trim().isEmpty()) { where AND (order_no LIKE ? OR remark LIKE ?) ; } String sql SELECT * FROM express_order where ORDER BY create_time DESC LIMIT ?, ?; // 占位符顺序keyword, keyword, offset, limit // ps.setString(1, % keyword %); // ps.setString(2, % keyword %); // ps.setInt(3, (pageNum - 1) * pageSize); // ps.setInt(4, pageSize); }LIMIT 两个参数的含义要分清第一个是偏移量从 0 开始第二个是每页条数。翻到第二页时偏移量是 pageSize第三页是 2 倍 pageSize。新手最容易把第一个参数直接写成页码结果第二页永远查不出数据。keyword 两侧的 % 符号要在传参时拼好不要在 SQL 字符串里写 %?%PreparedStatement 的占位符不支持这种写法。模糊查询在大数据量下会让索引失效但待接单列表本身有 status 索引课设规模下不用太担心。4. Eclipse 部署与启动从导入项目到首屏无报错代码写完真正折磨人的是环境。Eclipse Tomcat MySQL 这套组合环境上任何一个细小差异都会让项目起不来所以这一章把部署顺序和验证点写清楚照着走能省去很多无效排查。4.1 项目导入与目录结构核对拿到这类项目后的第一件事是看目录结构确认它是 Eclipse 动态 Web 项目而不是普通 Java 项目。正常结构是这样的express_web/ ├── src/ │ ├── servlet/ │ ├── service/ │ ├── dao/ │ ├── util/ │ └── db.properties ├── WebContent/ │ ├── WEB-INF/ │ │ ├── lib/ │ │ └── web.xml │ ├── css/ │ ├── jsp/ │ └── index.jsp └── build/导入时用 File - Import - Existing Projects into Workspace不要直接复制文件夹到工作区再打开那样 Eclipse 不会生成 .project 配置。导入后如果 Java 文件爆红先到 Properties - Java Build Path 里看 Libraries 是否完整再检查 Project Facets 里 Dynamic Web Module 版本和本机 Tomcat 版本是否匹配。如果看到大量 “HttpServlet cannot be resolved to a type”说明 Tomcat Server Runtime 没加进构建路径。右键项目 - Build Path - Libraries - Add Library - Server Runtime选中你配置好的 Tomcat 即可。这一步是把 Tomcat 运行时关联到项目不要把 servlet-api.jar 手动塞进 WEB-INF/lib否则会和容器自带的类冲突。4.2 MySQL 初始化与连接配置我习惯把建表 SQL 存成 db.sql用命令行一次性导入避免在 Navicat 里手动执行漏掉某条语句mysql -u root -p db.sql导入后执行 SHOW TABLES 验证两张表再看一下建表语句里的 ENGINE 是不是 InnoDB。如果 MySQL 初始化配置把默认引擎设成了 MyISAM建出来的表不支持事务第五章的事务坑必然复现。连接配置统一放在 src/db.properties不要散落在每个 DAO 类里jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/express_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password你的数据库密码如果你用的 mysql-connector-java 是 8.x驱动类写 com.mysql.cj.jdbc.Driver旧驱动类 com.mysql.jdbc.Driver 也能兼容但会打印一行过时警告。建议直接换新驱动别为了消警告来回折腾。4.3 Tomcat 运行配置与启动验证Eclipse 里配置 Tomcat 的步骤Window - Preferences - Server - Runtime EnvironmentsAdd 本地 Tomcat 安装目录再到 Servers 视图新建 Server 实例。项目右键 Run As - Run on Server部署启动。启动成功的标志是控制台出现 “Server startup in xxxx ms”并且浏览器能打开 http://localhost:8080/express_web/ 的登录页。如果启动卡住不动多半是某个 Servlet 的 init 方法里连了数据库而 MySQL 没启动或 db.properties 密码不对打开控制台会看到 Access denied for user 的记录顺着这个信息改配置即可。启动后务必实际走一遍注册到发单的流程再算完成。控制台没有报错只代表容器起来了业务通不通要看页面能否正常提交数据、数据库里能否查到新记录。5. 避坑时区、乱码、并发与事务的五个真实翻车点这一章记录我在复现这类课设时真实踩过的坑按“现象 - 原因 - 解决”排列照着排查能省下大量时间。5.1 环境级报错驱动缺失、时区校验与端口冲突第一条ClassNotFoundException: com.mysql.cj.jdbc.Driver。现象是项目启动后一操作订单就 500日志里明确找不到驱动类。原因是 JDBC jar 没打进部署包或者打进 WEB-INF/lib 了但 Eclipse 的 Deployment Assembly 没同步。解决方法右键项目 - Properties - Deployment Assembly确认 mysql-connector-java 出现在 WEB-INF/lib 列表里。只加 Java Build Path 不加 Deployment Assembly编译期不报错运行期必然翻车。第二条Communications link failure日志里能看到 “The server time zone value” 之类的字样。现象是本地数据库用命令行能进项目连不上。原因是 MySQL 8.x 驱动要求显式指定时区URL 里没写 serverTimezone驱动在握手阶段直接拒绝连接。解决方法是 URL 写成上面 4.2 节的样子。这里的 characterEncoding 解决字符集转换乱码serverTimezone 解决时区握手校验两个参数各管一摊不要省。第三条Address already in use: JVM_Bind。现象是 Tomcat 启动日志报端口被占用。原因通常是前一个 Eclipse 工作区里的 Tomcat 没关干净或本机其他程序占了 8080。解决方法是 Windows 下跑netstat -ano | findstr 8080查出 PID再taskkill /PID 进程号 /F结束占用进程。顺手把浏览器里旧端口的登录 Cookie 清掉不然换端口后容易出现登录态异常。5.2 数据正确性中文乱码、并发丢单与事务失效第四条页面输入中文落库变问号。现象是前端填“放到西区门口”库里变成 ???。原因是连接 URL 没带 characterEncoding或表字符集不是 utf8mb4。解决方法是三层齐改库、表、连接 URL 全部统一为 utf8mb4缺一层都可能出问题。已经是废数据的表不建议 ALTER 碰运气直接重建再插一次数据更省时间。这个坑在第 2 章建表时提前规避了所以建库脚本里特意写了 DEFAULT CHARSET。第五条两个账号同时点接单都提示成功。现象是页面都显示“订单已接”库里最终只有一个人的 id。原因是写成了“先 select 查 status再 update”两个连接都查到 status0然后各自执行 update后提交的覆盖了先提交的。解决方法是直接用 3.2 节那条 WHERE status0 的原子 update通过受影响行数判断是否成功。这个是一个需要记住的思想select 查状态只适合展示不适合作为写入权限的判断依据。第六条接单成功但订单里没有接单人。现象是状态变成已接单taker_id 却是 NULL。原因是两条 update 在自动提交模式下各成独立事务第一条正常提交第二条执行到一半报错回滚数据就残了。解决方法是把“更新状态”和“写入接单人”放进同一个手动事务里用 setAutoCommit(false) 包裹finally 块中不要忘了在正常路径下 commit。我见过有人把 commit 写在 catch 块里事务永远不提交这种低级错误倒是不常见但遇到了会非常难排查。6. 从课设到能跑的成品三个低成本小改进到这个阶段系统已经能完整演示了。如果还想多拿点分我建议做三件事每件成本都在半小时以内。第一给待接单订单加一个“超时未接单自动取消”。不用引入定时任务框架用启动类里的 TimerTask 就能扫完。每分钟执行一次下面的 SQL把超过 30 分钟还没人接的订单置为已取消UPDATE express_order SET status 5, update_time NOW() WHERE status 0 AND create_time DATE_SUB(NOW(), INTERVAL 30 MINUTE);这个 SQL 要放在事务里执行并且 WHERE 条件带上 status0防止把已接单订单也扫进去。Timer 的缺点是重启服务后会有部分漏单课设答辩场景完全够用想做得更规范可以换 ScheduledExecutorService。第二给“确认送达”加一道幂等校验。发起人可能连续点两次确认第二次应该返回“订单已关闭”而不是再次改状态。实现方式还是老套路更新语句里带上 status3 条件受影响行数为 0 就直接提示。这个思路覆盖了所有状态转移操作接口是不是幂等就看更新条件里有没有带当前状态。第三写一个并发验证的小脚本两个线程同时接同一个订单断言更新成功的次数之和等于 1。这个验证比口头说“支持并发控制”更有说服力演示时给导师看运行结果比说一堆术语直接得多。从那以后我每次复现这类系统都强制自己先建表、再写状态机、最后才写页面。顺序一乱界面做完才发现字段少设计了回头的成本就是三四张表全改。希望这篇拆解对你有帮助把资源下载下来照着搭一遍比只看不练更能发现问题。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询