news 2026/10/29 15:05:05

快递e栈JavaWeb实战:Servlet+Filter+MVC取件闭环

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
快递e栈JavaWeb实战:Servlet+Filter+MVC取件闭环

简介:面向 JavaWeb 入门与进阶学习者的综合实战项目源码包,以「快递e栈」快递代收业务为背景,把 jQuery、Bootstrap、Layui、Ajax 等前端技术与 MySQL、Servlet、Filter、Listener、JSP 及 MVC 分层组织成完整链路,适合课程设计、毕设选题或框架练手,也便于体会 session 会话管理与 filter 请求拦截的实际用法。压缩包共 831 个文件、约 17.23MB:前端由 58 个 HTML 页面、72 个 JS 脚本、38 个 CSS 样式表及 png、gif 界面素材组成;后端 52 个 Java 源文件与 109 个 class 对应控制层与 DAO 实现;另有 36 个 jar 依赖、XML 与 properties 配置,可导入 IDE 调试运行。目前已有 890 人学习下载。项目含用户与快递员双端登录、运单录入查询、状态流转等模块,可据此梳理数据库表设计、过滤器权限校验与 MVC 分层结构。

1. 从驿站柜台的排队说起:这套快递e栈要解决什么

驿站柜台前最怕两件事:件堆成山找不到,取件人报不出单号。一个中等规模的站点一天三百到八百件,靠纸本登记加群消息喊话,丢件、错拿、超期件几乎必然发生,月底对账时谁也说不清哪一单卡在哪个环节。这套「快递e栈」综合项目要解决的就是这条链路:把包裹入库、取件码生成、到柜核销、超期统计串成一个能真正跑起来的 Web 系统,店员在浏览器里完成收件与交付,收件人凭一串六位码取件,管理员能看到当天的入库量和未取件清单。

技术上走的是最正统的 JavaWeb 路线:Servlet 承担控制器,Filter 做统一拦截,Session 管登录态,JSP 承担视图,MVC 分层清楚,没有框架遮蔽底层细节。它适合两类人:一是要交 JavaWeb 课程设计或毕设的学生,需要一个业务闭环完整、代码量可控、答辩时能把分层讲明白的题目;二是长期写框架、想回头把 Servlet 生命周期和请求链路补一遍的开发者。下面按需求、架构、实现、避坑、验收五段推演,每一步都给到能照着改的参数和代码。

2. 需求拆解与技术选型:三个角色与六条主流程怎么定

动手写代码之前,最费时间的往往不是写,而是想清楚"到底有几个人用、每个人能干什么"。很多同学一上来就建表,写到一半发现取件码该谁生成、超期件该不该锁柜,回头改表改接口,血泪经验。这一章先把角色和流程固定下来,再解释为什么这套项目不该上框架。

2.1 三个角色与六条主流程

角色只有三个,别再加了:收件人(普通用户)、驿站店员(操作员)、站点管理员。收件人不注册,凭手机号后四位加取件码到柜取件,这样能省掉一整套用户注册登录的复杂度;店员负责入库、核销、异常登记;管理员负责账号管理、货架位配置和统计报表。

主流程六条,建议直接抄进需求文档:

  1. 登录与登出。店员和管理员共用一张用户表,靠 role 字段区分,登录成功后写 Session。
  2. 快递入库。录入运单号、快递公司、收件人手机号,系统生成六位取件码,分配货架位,状态置为在库。
  3. 取件核销。输入取件码或扫运单号,校验状态为在库后置为已取件,同时写一条取件记录。
  4. 列表查询。按运单号、手机号后四位、取件码、状态、时间区间组合筛选,分页展示。
  5. 超期处理。入库超过设定天数未取,状态标记为超期,列表中高亮,支持导出。
  6. 数据统计。当日入库量、当日取件量、在库总数、超期数,给管理员一个总览页。

这六条流程对应六组接口,每组接口背后是一到两个 Servlet。需求拆到这个粒度,后面的表结构和分层才有依据。我的习惯是每条流程都写一句验收标准,比如"入库成功后,列表页第一页第一条必须能查到该运单号",写不出验收标准的功能,基本就是自己都没想明白。

2.2 技术选型:为什么这套项目不该上框架

这个问题答辩时一定会被问:"为什么不用 Spring Boot?" 答案不应该是"老师要求",而应该是有取舍的技术判断。

第一,教学目标和工程目标不同。这个项目的核心交付物是"看得见请求怎么进来、怎么被拦截、怎么走到数据库、怎么回到页面"。Servlet 的 init/service/destroy 生命周期、Filter 链的执行顺序、Session 的创建时机,这些东西在框架里被封装得太好,写一年也未必碰得到。这个项目正好是把它们摊开来看的载体。

第二,Filter 是这套系统里性价比最高的技术点。编码统一、登录校验、权限区分、访问日志、响应头设置,五个横向关注点全部塞进 Filter 链,业务 Servlet 里一行都不用写。这个设计在答辩时非常加分,因为它证明你理解了"横切关注点"这个概念,而不是把所有逻辑堆进 Servlet。

第三,Session 撑起登录态足够用。单机部署、几十个店员同时在线,Session 内存存储完全扛得住,没有引入 Token 和分布式的必要。真正要讲清楚的是 Session 里放什么、超时怎么配、什么情况下会被销毁,这些细节比换一套鉴权方案更有价值。

技术栈清单大致如下,版本以你本地能跑通为准,不必追新:

层次技术承担职责
表现层JSP + JSTL + 原生 JS页面渲染、表单提交、列表分页条
控制层Servlet 3.1+接收请求、参数校验、流程编排、选择视图
拦截层Filter编码、鉴权、权限、日志
会话层HttpSession登录用户信息、验证码、提示消息
业务层普通 Java 类(Service)事务边界、业务规则、状态流转
持久层JDBC + PreparedStatementSQL 执行、结果集映射
数据存储MySQL用户、包裹、取件记录、货架

JDBC 直连而不是上 ORM,理由同上:SQL 写得出来、能优化、能解释索引,是这个阶段的基本功。而且这套项目的查询条件组合多,手写动态 SQL 反而更直观。

2.3 工程结构与包组织

目录结构建议按分层切,不要按功能切。按功能切在项目小的时候看着清爽,一旦某个功能膨胀就很难受;按层切配合包名,新人接手时能立刻定位。

express-station/ ├── src/main/java/com/example/station/ │ ├── controller/ # Servlet:LoginServlet、ParcelInboundServlet... │ ├── service/ # 业务逻辑与事务边界 │ ├── dao/ # JDBC 数据访问,一个表一个 DAO │ ├── entity/ # User、Parcel、PickupRecord、Shelf │ ├── filter/ # EncodingFilter、AuthFilter、LogFilter │ ├── util/ # DBUtil、PickupCodeGenerator、PageResult │ └── common/ # 常量、枚举、统一响应封装 ├── src/main/webapp/ │ ├── WEB-INF/views/ # JSP 页面,放 WEB-INF 下防止直接访问 │ ├── css/ js/ images/ │ └── web.xml # 或在 Servlet 上用 @WebServlet 注解 └── pom.xml

JSP 一律放在WEB-INF/views下,这是个容易被忽略的细节:放在 webapp 根目录下,用户可以直接输 URL 访问页面,绕过 Servlet 里的数据准备逻辑,轻则页面报空指针,重则暴露未鉴权的数据。放进 WEB-INF 之后,页面只能由 Servlet 内部 forward 过去,访问路径就收敛了。

包名用com.example开头,别用真实公司域名;类名保持"名词 + 角色"的格式,ParcelDao、ParcelService、ParcelInboundServlet,一眼能看出职责。工具类不要做成万能工具箱,DBUtil只管连接和关闭,PickupCodeGenerator只管发码,职责越窄越不容易在后期被改烂。

3. 总体架构设计:MVC 三层与四张核心表的职责划分

架构设计这一章要解决的问题是"请求从浏览器出发,经过哪些站,最后怎么回到页面上"。把这条路径画清楚(文字描述即可,不必画图),后面写任何一个模块都不会迷路。

3.1 一次请求在 MVC 三层里的完整路径

以"店员提交入库表单"为例,完整路径是这样的:

浏览器 POST 表单 → 容器按 web.xml 或注解匹配到ParcelInboundServlet→ 先经过EncodingFilter(设置 UTF-8)→ 再经过AuthFilter(检查 Session 里有没有登录用户,没有就重定向到登录页)→ 进入 Servlet 的 doPost → 收集参数并做基础校验(运单号非空、手机号 11 位)→ 调用ParcelService.inbound(...)→ Service 开启事务,调用PickupCodeGenerator发码,调用ParcelDao插入记录,调用ShelfDao更新货架占用,提交事务 → 返回结果对象 → Servlet 把结果放进 request 属性,forward 到parcel_list.jsp→ JSP 用 JSTL 渲染。

这条路径上有三个关键约定,建议在项目一开始就定死:

  • Servlet 不写业务规则。比如"同一运单号不能重复入库"这条规则,必须放在 Service,不能放在 Servlet 里判。放 Servlet 里的后果是:换一个入口(比如批量导入)就得复制一遍判断,迟早不一致。
  • Service 不碰 request 和 response。Service 的方法签名只接受普通 Java 对象,返回普通 Java 对象。这样 Service 可以被单元测试,也可以被定时任务复用。
  • DAO 不写业务判断。DAO 只负责"执行 SQL、映射结果",条件更新影响行数为 0 要不要抛异常,是 Service 的事。

三层之间的数据传递用实体类加一个统一的结果封装。别用 Map 传,Map 传值三个月后你自己都不知道里面有哪些 key。

3.2 四张核心表与取件码字段设计

表不在多,四张就够:用户表、包裹表、取件记录表、货架表。下面这张字段表可以直接对着建。

表名关键字段说明
sys_userid, username, password_hash, salt, real_name, phone, role, statusrole 取值 OPERATOR/ADMIN,status 控制启用停用
parcelid, waybill_no, company, receiver_phone, shelf_no, pickup_code, status, in_time, pickup_time, operator_idstatus:0 在库 / 1 已取件 / 2 超期
pickup_recordid, parcel_id, pickup_code, operator_id, pickup_time, channelchannel 区分柜台核销与自助取件
shelfid, shelf_no, capacity, usedused 与 capacity 用于分配货架位

建表语句示例,注意索引和唯一约束:

CREATE TABLE parcel ( id BIGINT PRIMARY KEY AUTO_INCREMENT, waybill_no VARCHAR(32) NOT NULL COMMENT '快递运单号', company VARCHAR(20) NOT NULL COMMENT '快递公司简称', receiver_phone VARCHAR(11) NOT NULL COMMENT '收件人手机号', shelf_no VARCHAR(12) NOT NULL COMMENT '货架位编号,如 A-03-12', pickup_code CHAR(6) NOT NULL COMMENT '六位取件码', status TINYINT NOT NULL DEFAULT 0 COMMENT '0在库 1已取 2超期', in_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, pickup_time DATETIME NULL, operator_id BIGINT NOT NULL COMMENT '入库操作员', UNIQUE KEY uk_waybill (waybill_no), UNIQUE KEY uk_pickup_code (pickup_code, status), KEY idx_phone (receiver_phone), KEY idx_status_time (status, in_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

三点说明。第一,waybill_no加唯一索引,把"重复入库"这件事从应用层判断变成数据库兜底,并发下也可靠。第二,pickup_code上的唯一索引带上了 status,意思是"同一个取件码在在库状态下只能存在一份",已取件的记录不占用码位,这样六位码的复用空间就够用了。第三,idx_status_time这个联合索引是为列表页和超期扫描准备的,查询条件里 status 和 in_time 经常一起出现,单列索引效果会差不少。

3.3 Session 里到底该放什么

登录成功后往 Session 里塞什么,直接决定后面鉴权写起来顺不顺。我的建议是只放一个轻量的登录对象,不要放整个 User 实体:

public class LoginUser implements Serializable { private Long userId; private String username; private String realName; private String role; // OPERATOR / ADMIN private Long loginTime; // 毫秒时间戳,用于超时提示和重复登录判断 // getter / setter 省略 }

Session 里只放loginUser、验证码文本、一次性的提示消息这三个 key。绝对不要把密码、密码哈希、盐值放进去;也不要放整个用户对象后又在页面上直接遍历它的属性,一旦哪天给 User 加了敏感字段就跟着泄露。

超时时间在web.xml里配:

<session-config> <session-timeout>30</session-timeout> <cookie-config> <http-only>true</http-only> </cookie-config> </session-config>

30 分钟是驿站场景比较合适的值:店员中午轮休回来还在,晚上下班后自动失效。http-only打开,防止前端脚本读到会话 Cookie。另外注意一个容易踩的点:Session 的超时是"最后一次访问后计时",不是"登录后固定 30 分钟",如果需求是后者,得自己在LoginUser里存 loginTime 并在 Filter 里比对。

4. 核心模块实现:从登录拦截到取件核销的四个关键动作

前面两章把需求和结构定完了,这一章落到代码。四个动作按依赖顺序排:先有登录和拦截,才能谈业务;先有入库发码,才能谈核销;核销之后才谈列表和分页。

4.1 登录鉴权 Filter:一次配置,全局生效

AuthFilter是整个项目里最该写好的一段代码,因为它同时承担了安全边界和用户体验。

@WebFilter(urlPatterns = "/*") public class AuthFilter implements Filter { // 白名单:登录页、登录接口、静态资源 private static final List<String> WHITE_PREFIX = Arrays.asList("/login.jsp", "/login", "/css/", "/js/", "/images/"); @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 去掉上下文路径,得到应用内相对路径,便于白名单匹配 String uri = request.getRequestURI().substring(request.getContextPath().length()); if (isWhite(uri)) { chain.doFilter(req, resp); return; } // getSession(false):未登录请求不创建 Session,避免内存被无效会话占满 HttpSession session = request.getSession(false); LoginUser loginUser = session == null ? null : (LoginUser) session.getAttribute("loginUser"); if (loginUser == null) { // 记住原始目标地址,登录后跳回来 session = request.getSession(true); session.setAttribute("backUrl", uri); response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } // 管理员页面二次校验:路径以 /admin/ 开头但角色不是 ADMIN 的,一律拒绝 if (uri.startsWith("/admin/") && !"ADMIN".equals(loginUser.getRole())) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(req, resp); } private boolean isWhite(String uri) { return WHITE_PREFIX.stream().anyMatch(uri::startsWith); } }

这段代码里有几个决定成败的细节。getSession(false)而不是getSession()是关键:后者在会话不存在时会新建一个,结果就是每个未登录请求都留下一个空 Session,被扫描器刷一遍内存就上去了。白名单用startsWith匹配前缀而不是equals精确匹配,是因为静态资源路径带文件名,精确匹配写不全。backUrl的记录放在重定向之前,登录成功后从 Session 取出来再删掉,用户体验会好很多,这也是答辩时容易被追问的点。权限二次校验放在 Filter 里而不是每个 Servlet 里各写一遍,是为了保证新增管理页面时不会漏。

编码 Filter 建议单独写一个EncodingFilter,并且排在 AuthFilter 前面,顺序通过@WebFilter的声明或者 web.xml 里 filter-mapping 的先后控制。顺序反了会出现"中文用户在未登录时跳转链接里的参数乱码"这种玄学问题。

4.2 快递入库:取件码生成与唯一性兜底

取件码是这个系统里最容易被低估的设计点。用自增 ID 取模、用时间戳后六位,都是常见做法,但在真实场景里都会出问题:前者能被人猜出规律,后者在批量入库时会撞码。

public class PickupCodeGenerator { // 去掉 0/O、1/I/L 等肉眼易混字符,减少柜台报码听错的情况 private static final char[] POOL = "23456789ABCDEFGHJKMNPQRSTUVWXYZ".toCharArray(); private static final SecureRandom RANDOM = new SecureRandom(); private static final int LENGTH = 6; private static final int MAX_RETRY = 5; public String next() { StringBuilder sb = new StringBuilder(LENGTH); for (int i = 0; i < LENGTH; i++) { sb.append(POOL[RANDOM.nextInt(POOL.length)]); } return sb.toString(); } }

配合 Service 里的重试逻辑:

public Long inbound(ParcelForm form, Long operatorId) throws BusinessException { for (int i = 0; i < MAX_RETRY; i++) { String code = codeGenerator.next(); if (parcelDao.existsPickupCode(code)) { continue; // 撞码,换一个重试 } String shelfNo = shelfService.allocate(); // 按剩余容量分配货架位 Parcel parcel = Parcel.of(form, shelfNo, code, operatorId); try { return parcelDao.insert(parcel); // 唯一索引兜底,冲突则抛异常 } catch (DuplicateKeyException e) { // 并发下两条线程同时拿到同一个码,靠数据库唯一索引拦下,继续重试 continue; } } throw new BusinessException("取件码分配失败,请重试"); }

这里的设计逻辑值得讲一遍:字符池剔除了易混字符,是为了柜台场景的可用性——店员念码、老人听码,少一个歧义就少一次返工。32 个字符取 6 位,空间约十亿级,站点日入库八百件,码的复用压力很小。查重加唯一索引是双保险:查重解决绝大多数情况,唯一索引解决并发竞态。捕获DuplicateKeyException后重试而不是直接报错,是因为这种情况属于预期内的正常冲突,不该让用户看到错误页。

parcelDao.insert里应该用PreparedStatement并取回自增主键:

try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(INSERT_SQL, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, parcel.getWaybillNo()); ps.setString(2, parcel.getCompany()); ps.setString(3, parcel.getReceiverPhone()); ps.setString(4, parcel.getShelfNo()); ps.setString(5, parcel.getPickupCode()); ps.setLong(6, parcel.getOperatorId()); ps.executeUpdate(); try (ResultSet rs = ps.getGeneratedKeys()) { return rs.next() ? rs.getLong(1) : null; } }

try-with-resources保证连接和语句一定关闭,这是 JDBC 项目里最常见的翻车点之一,后面避坑章节还会提。参数一律用setXxx绑定,绝不拼字符串,这一条在答辩现场被要求解释 SQL 注入时能直接救命。

4.3 取件核销:用状态机防住重复取件

核销看起来只是"把状态改成 1",实际是个并发问题。两个店员同时处理同一位取件人,或者店员手快点了两次提交,都可能出现同一件包裹被核销两次、写出两条取件记录的情况。

正确做法不是先查再改,而是把判断条件写进 UPDATE 语句本身:

UPDATE parcel SET status = 1, pickup_time = NOW() WHERE id = ? AND status = 0;

Service 里判断受影响行数:

public void pickup(Long parcelId, Long operatorId) throws BusinessException { int affected = parcelDao.markPicked(parcelId); if (affected == 0) { // 行数为 0 有两种可能:包裹不存在,或状态已经不是"在库" Parcel current = parcelDao.findById(parcelId); if (current == null) { throw new BusinessException("包裹不存在"); } throw new BusinessException("该包裹当前状态为" + StatusEnum.of(current.getStatus()).getLabel() + ",无法重复核销"); } pickupRecordDao.insert(parcelId, operatorId, "COUNTER"); }

WHERE status = 0这个条件是把乐观锁写在了 SQL 里:数据库对同一行的更新是串行的,第一条执行成功后 status 变成 1,第二条的 WHERE 匹配不上,影响行数就是 0,业务层据此给出明确提示。这比加锁简单,也比"先 select 再 if 再 update"可靠。血泪经验是,凡是"状态只能单向流转一次"的场景,都应该用条件更新,而不是应用层判断。

如果核销和写取件记录需要保证原子性,把这两步放进同一个事务:

try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); try { int affected = parcelDao.markPicked(conn, parcelId); if (affected == 0) throw new BusinessException("状态已变更,无法核销"); pickupRecordDao.insert(conn, parcelId, operatorId, "COUNTER"); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } }

注意这里的 DAO 方法签名多了一个Connection参数。事务要跨两个 DAO,就必须让它们共用同一个连接;每个 DAO 自己开连接的事务是假事务,这是新手最常犯的错,后面单独讲。

4.4 列表查询与分页:动态条件加固定排序

列表页是使用频率最高的页面,查询条件组合多,写不好会又慢又乱。核心是两件事:动态条件用StringBuilder拼 WHERE 片段,参数一律走占位符;排序和分页永远加在最后。

public PageResult<Parcel> query(ParcelQuery q, int pageNo, int pageSize) { StringBuilder where = new StringBuilder(" WHERE 1 = 1 "); List<Object> params = new ArrayList<>(); if (StringUtils.hasText(q.getWaybillNo())) { where.append(" AND waybill_no LIKE ? "); params.add("%" + q.getWaybillNo().trim() + "%"); } if (StringUtils.hasText(q.getPhoneTail())) { where.append(" AND receiver_phone LIKE ? "); params.add("%" + q.getPhoneTail().trim()); // 只匹配后缀,可用索引前缀扫描 } if (q.getStatus() != null) { where.append(" AND status = ? "); params.add(q.getStatus()); } if (q.getStartTime() != null) { where.append(" AND in_time >= ? "); params.add(q.getStartTime()); } // 先查总数,再查当页;总数用于渲染分页条 long total = parcelDao.count(where.toString(), params.toArray()); List<Parcel> rows = parcelDao.page(where.toString(), params.toArray(), (pageNo - 1) * pageSize, pageSize); return new PageResult<>(rows, total, pageNo, pageSize); }

分页 SQL 用LIMIT ?, ?,两个参数分别是偏移量和条数,同样用占位符绑定,不要拼进字符串。排序固定为ORDER BY in_time DESC, id DESC,加上 id 是为了在入库时间相同时保证顺序稳定,否则翻页时可能出现同一条记录重复出现或漏掉。这个细节很多课程设计都没做,答辩时提一句能显出你想过。

页大小做成常量,默认 20,上限 100。上限的作用是防止有人手工改 URL 传个 100000 进来,一次性把整张表拉出来。

5. 避坑与排查:这套项目最容易翻车的五个点

写了这么多年代码,这个项目的坑基本集中在固定的几个地方。下面五条按出现频率排,每条按"现象 → 原因 → 解决"给。

第一条:中文全是问号或者乱码。现象是表单提交后姓名、备注变成???,或者 GET 请求的查询参数乱码。原因通常是三处不一致:JSP 页面没声明pageEncoding="UTF-8"、请求体没设编码、数据库连接串没带字符集。解决要一路查到底:JSP 头部加<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>;EncodingFilter里request.setCharacterEncoding("UTF-8")和response.setContentType("text/html;charset=UTF-8")都写;连接串加上useUnicode=true&characterEncoding=utf8;建库时字符集用utf8mb4。注意setCharacterEncoding必须在第一次读取参数之前调用,所以必须放在 Filter 里且排在最前,放在 Servlet 里就晚了。

第二条:Filter 拦了登录页,页面无限重定向。现象是浏览器报"重定向次数过多",或者登录页的 CSS 全部 404。原因有两个:白名单里漏了静态资源目录,导致 CSS 请求被重定向到登录页,浏览器把登录页当样式表解析;或者白名单判断写错,登录页本身也被拦。解决是把白名单做成前缀列表并打印日志,先确认uri到底取成了什么值——很多时候是getRequestURI()忘了减掉getContextPath(),前缀多了个项目名,怎么匹配都对不上。排查这类问题最快的方法是临时在 Filter 里加一行System.out.println(uri),看一眼就明白了。

第三条:核销成功了,但取件记录没写进去,重启后数据对不上。现象是包裹列表显示已取件,取件记录表里查不到对应行。原因是核销和写记录用了两个不同的数据库连接,各自自动提交,第一条成功第二条失败就成了半截操作。解决是把两步放进同一个 Connection 并显式控制事务,失败时 rollback。这个坑最隐蔽的地方在于它平时不出现,只有第二条 SQL 报错时才暴露,所以上线前一定要人为制造一次异常验证回滚是否生效。

第四条:跑一天之后数据库报连接数超限。现象是页面开始卡顿,日志里出现Too many connections或者Communications link failure。原因基本是 DAO 里开了连接没关,异常路径上更是完全漏掉。解决是统一用try-with-resources,Connection、PreparedStatement、ResultSet三层全部包住,并且在DBUtil里把关闭逻辑写死成模板方法。如果用了连接池,记得在ServletContextListener的contextDestroyed里关闭数据源,否则每次热部署都会泄漏一批连接,这种坑在开发阶段特别容易反复出现。

第五条:页面刷新一次就重复入库一条。现象是提交入库表单后按 F5,包裹表里多出一条一模一样的记录,运单号唯一索引直接报错。原因是请求完成后用了 forward 而不是 redirect,浏览器地址栏还停在 POST 的 URL 上,刷新就是重放 POST。解决是提交成功后统一response.sendRedirect(...),也就是 PRG 模式(Post-Redirect-Get),让浏览器重新发一个 GET 请求。如果业务上必须防重复提交,再在 Session 里存一个一次性 token,表单携带,提交后立即失效。两条一起做,双保险。

6. 验收与答辩:用一张检查表证明它真的能用

演示跑通不等于方案合格。答辩现场最常出现的场景是:功能都能点,但一问边界就答不上来。我一般会提前准备一张验收检查表,按"动作 → 预期 → 观察点"组织,自己先走一遍,再拿它当答辩提纲。

检查项操作动作预期结果与观察点
未登录访问清空 Cookie 后直接输业务页 URL跳转登录页,登录后回到原地址
越权访问用店员账号访问/admin/路径返回 403,不放行
重复入库同一运单号连续提交两次第二次给出明确提示,数据库无重复行
取件码冲突人工在库里插入已存在的取件码后入库自动换码成功,不报错
重复核销同一包裹连续核销两次第二次提示"状态不允许",记录只有一条
中文检索用含中文的快递公司名筛选结果正确,无乱码
SQL 注入运单号框输入' OR '1'='1返回空结果,不返回全表
会话超时登录后等待超过 30 分钟再操作跳转登录页,而非报空指针
大数据量造两千条在库记录后翻页每页 20 条,翻到最后一页不重不漏

表格里每一行都能对应到前面某一章的设计:越权对应 Filter 的二次校验,重复核销对应条件更新,注入对应 PreparedStatement,会话超时对应session-timeout。答辩时不要按功能菜单顺序演示,按数据流顺序演示——先入库,看列表,再取件,看记录,最后看统计,评委的接受度会高很多。中间穿插一次"故意失败":比如输一个不存在的取件码,展示系统给的是友好提示而不是 500 错误页。这比展示十个成功案例更有说服力,因为它证明你处理过异常路径。

评委最喜欢追问的三类问题,提前准备好答案:第一类是"并发下会不会出问题",答案落在唯一索引和条件更新上;第二类是"Session 和 Cookie 的区别,会话什么时候销毁",答案落在getSession(false)和超时配置上;第三类是"如果数据量涨到十万条怎么优化",答案落在联合索引、分页和避免SELECT *上,不需要吹分布式。

从那以后我每次带这类项目,都会在答辩前一天强制走一遍这张表,凡是过不了的项,当晚改完再睡。因为现场翻车的代价远高于提前半小时的检查。这套快递e栈的完整工程结构、四张表的建表脚本、Filter 链与核销事务的实现代码都在方案里整理好了,可以直接拿去改包名和库名复用。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 15:04:36

superpowers不是工具,而是开发者效率能力组合的实用指南

我见过太多人把“superpowers”当成一个可以下载安装的软件包来搜&#xff0c;甚至有人会问“这个工具到底在哪个商店里”。说实话&#xff0c;我第一次看到这个词也愣了一下&#xff0c;但折腾过几轮之后我彻底想明白了&#xff1a;superpowers不是一个软件&#xff0c;不是某…

作者头像 李华
网站建设 2026/10/29 15:04:19

Multi-Agent系统核心技术解析与商业实践

1. Multi-Agent平台的核心竞争格局分析过去一年里&#xff0c;我亲眼见证了超过20个团队从零开始搭建Multi-Agent系统。这个领域的竞争正在从单纯的技术比拼&#xff0c;转向更复杂的生态位争夺。目前市场上的玩家主要分为三类&#xff1a;第一类是像AutoGPT这样的开源项目&…

作者头像 李华
网站建设 2026/10/29 15:04:11

Java与C++成员变量与方法修饰符顺序规范详解

1. 成员变量与成员方法修饰符顺序的重要性在面向对象编程中&#xff0c;成员变量和成员方法的修饰符顺序看似是个小问题&#xff0c;但实际上它影响着代码的可读性、一致性和维护性。就像我们整理衣柜时会把常穿的衣服放在容易拿到的地方一样&#xff0c;合理的修饰符顺序能让代…

作者头像 李华
网站建设 2026/10/29 15:02:25

系统架构设计师知识点集锦:从PDF到可检索知识库的整理与检索实践

简介&#xff1a;这份《2021-系统架构设计师知识点集锦》面向备考软考系统架构设计师的考生&#xff0c;尤其适合以自学方式推进复习、需要系统梳理考纲要点的人群。内容围绕系统架构设计核心知识展开&#xff0c;可用于搭建知识框架、定位薄弱环节与考前集中回顾。资源为单个P…

作者头像 李华
网站建设 2026/10/29 15:01:33

AI大模型微调技术:原理、实践与优化

1. AI大模型微调技术全景解析最近两年&#xff0c;AI大模型技术呈现爆发式增长&#xff0c;从最初的GPT-3到如今的Claude、Llama等系列模型&#xff0c;参数规模从百亿级跃升至万亿级。但直接使用这些"庞然大物"往往效果不尽如人意——要么无法精准适配特定业务场景&…

作者头像 李华
网站建设 2026/10/29 15:01:33

Python多任务编程:GIL与互斥锁的深度解析与优化

1. Python多任务编程中的GIL与互斥锁深度解析在Python开发中&#xff0c;多任务编程是提升程序性能的重要手段&#xff0c;但GIL&#xff08;全局解释器锁&#xff09;的存在让这个领域充满特殊性。作为从业十年的Python开发者&#xff0c;我见过太多因为不理解GIL和互斥锁关系…

作者头像 李华