“计算机毕业设计之jsp献血者信息管理系统”这个题目,乍一看特别像标准的“增删改查”项目:不就是把献血者的信息录入、编辑、删除,再分个页吗?但真正动手做的人会发现,这个题目最容易被低估的地方,不是“增删改查”本身,而是它背后一整套血液业务流程。我把这类题目拆解过的经验都写出来,包括业务建模、表结构设计、角色权限划分、状态流转、部署避坑和答辩演示顺序,希望能帮到正在做这个毕设的本科生,也想给想快速复现一个完整JavaWeb项目的初级开发者做个参考。
1. 先别急着写代码:把“献血者管理”的业务流程拆清楚
1.1 这个系统到底要管什么
我见过不少同学拿到题目后,第一反应是去设计“献血者表”,然后写一堆维护界面。做出来之后自己都觉得空——因为真正的献血业务是有一条完整链路的,你管的不是“一个人”的信息,而是“一个人从预约到血液被使用的全过程”。
一个能在答辩时讲得清楚的献血者信息管理系统,至少要涵盖四段业务:
- 献血者档案:姓名、性别、出生日期、身份证号、手机号、血型、RH阴性/阳性、通讯地址、紧急联系人、献血次数、最近献血日期。这是整个系统的基础数据。
- 预约与到场管理:献血者先预约时间,到现场后做健康征询和初筛。初筛项目包括血压、血红蛋白、转氨酶、血型快检等,只有初筛合格的人才能进入采血环节。
- 采血与检测结果管理:采血后血液样本要送检,检测项目包括乙肝、丙肝、艾滋、梅毒等传染指标。检测结果决定这袋血是“合格入库”还是“报废”。
- 库存与出库管理:合格的血液进入血库,按血型和成分分类存储;医院申请用血时再出库。部分系统还会记录血液的失效日期,超过保存期自动报废。
所以,“献血者信息管理系统”虽然听起来只写了“献血者”,实际上它是一个简化的血站业务系统。如果你想让它有亮点,就应该把上面这条链路完整地做出来,而不是只做一个单表维护页面。
1.2 三种角色对应三种界面
这类系统一般有三大类用户:管理员、工作人员(可以叫采血员/护士)、献血者本人。三者的需求完全不同,所以在设计菜单和页面时一开始就要分开:
- 管理员:用户管理、权限配置、数据统计、公告发布、系统参数维护。
- 工作人员:献血者登记、预约审核、初筛结果录入、采血记录登记、检测结果维护、血液入库/出库。
- 献血者:注册登录、维护个人资料、查看自己的献血记录、预约下一次献血。
页面结构上的做法是:按角色建立三个页面目录,比如/admin/、/staff/、/donor/,登录成功后根据用户角色跳转到对应主页,在公共的头部导航里动态显示菜单。在JSP里可以用<c:if test="${sessionScope.user.role == 'ADMIN'}">控制菜单显示,但更重要的还是服务端的权限校验,不能只靠页面隐藏来控制访问。
1.3 一条血液的“生命周期”是什么样
我建议你在动手写代码之前,先把状态流转图画在纸上。整个系统的核心数据状态大致是:
预约登记 -> 到场确认 / 爽约 -> 健康征询与初筛 -> 初筛通过与采血 -> 样本送检 -> 检测合格(入库)/ 检测不合格(报废) -> 库存管理 -> 医院申请用血 -> 出库这些状态在数据库里通常用一个status字段表示,取值为数字或简短的字符串。之所以不用中文直接存,是为了避免业务调整时还要改全表数据,也方便代码里做分支判断。比如预约记录的状态可以约定:
| 状态值 | 含义 |
|---|---|
| 0 | 待确认 |
| 1 | 已确认 |
| 2 | 已完成 |
| 3 | 已取消 |
献血记录的状态也可以约定:
| 状态值 | 含义 |
|---|---|
| 0 | 初筛中 |
| 1 | 已完成采血 |
| 2 | 检测中 |
| 3 | 检测合格 |
| 4 | 检测不合格 |
| 5 | 已入库 |
| 6 | 已出库 |
状态字段是这个系统里最容易出亮点的地方。答辩时老师大概率会问:“被检测为不合格的血液你怎么处理?”如果你能回答“状态置为不合格,不再参与库存统计,并且系统会记录报废原因,同时给献血者发送一条站内消息通知”,这个完整度立刻就不一样了。
2. 技术选型:为什么“老套”的JSP反而是稳妥方案
2.1 三种主流方案怎么选
既然题目里明确写了“jsp”,说明题目的考察重点就是最经典的JavaWeb技术栈。但实际做的时候,不少同学会纠结:要不要用Spring Boot?要不要做前后端分离?我把几种方案放在一起比较过,结论很明确:
| 方案 | 学习成本 | 与题目契合度 | 答辩容错率 | 建议 |
|---|---|---|---|---|
| 纯JSP + Servlet | 低 | 高 | 高,逻辑直观 | 最稳妥 |
| JSP + Servlet + JavaBean分层 | 中 | 高 | 高 | 推荐 |
| Spring Boot + JSP模板 | 中高 | 中,偏离“jsp”技术特征 | 中 | 不推荐 |
| Spring Boot + 前后端分离 | 高 | 低,几乎看不出JSP | 低 | 不做考虑 |
这里要解释一下背后的逻辑。毕业设计的核心是“完整地理解一个业务系统的实现”,不是“展示你用了多少框架”。你如果用了Spring Boot,确实省了很多配置,但老师问你“Servlet生命周期”的时候就容易露怯。反过来,你用纯JSP+Servlet把分页、事务、状态流转、权限校验都实现了,每一个环节都是自己写出来的,讲起来底气是完全不同的。
还有一种折中方案,我比较推荐:底层用JDBC+连接池,中间层用Servlet控制流程,表现层用JSP+JSTL渲染数据,模型层拆出Dao和Service。这套结构既保留了JSP题目要求,又有清晰的分层意识,在毕业设计里属于很标准的分层写法。
2.2 我推荐的目录结构与请求规范
一个清晰的工程目录,比多写两行代码更重要。我当时按下面的结构组织项目:
WebContent ├── admin │ ├── index.jsp │ ├── donorList.jsp │ └── statistics.jsp ├── staff │ ├── index.jsp │ ├── registerDonor.jsp │ └── bloodInList.jsp ├── donor │ ├── index.jsp │ └── myRecord.jsp ├── common │ ├── header.jsp │ └── footer.jsp ├── WEB-INF │ ├── web.xml │ └── lib └── static ├── css └── js请求路径的规范我建议统一成*.do格式,比如:
/donor/list.do打开献血者列表/donor/add.do新增献血者/donor/edit.do编辑献血者/donor/delete.do删除献血者/appointment/confirm.do工作人员确认预约
这样一个Servlet对应一个功能模块,而不是一个Servlet处理所有请求,出问题时也方便排查。
2.3 为什么不做前后端分离
我能理解一部分同学想用Vue+接口的方式来做,页面确实更“现代”一点。但在毕业设计这个场景下,前后端分离会带来几个现实问题:跨域配置要处理、接口权限要单独设计、两套工程要分别部署、答辩时要同时打开前端工程和后端工程。很多人在这一堆额外事项里消耗了大量时间,最后页面交互还是很粗糙。
JSP的方案天然适合做表格型数据展示:后台查完数据放到request域里,JSP用JSTL循环输出,刷新页面就是最新的数据,不需要异步接口。对献血者信息这种典型的数据录入+查询系统,服务端渲染反而更直接。想加分的话,只需要在统计页面用一小段ECharts做图表就够了,完全没必要把整个项目拆成前后端分离。
3. 数据库设计:五张核心表把系统撑起来
3.1 核心表之间的关系
数据库是这类系统的命门。表结构设计得好,后面所有代码都好写;表结构乱,功能做一半就得返工。一套完整的献血者信息管理系统,我建议从下面六张表起步:
| 表名 | 作用 | 与其他表的关系 |
|---|---|---|
| user | 系统用户(管理员、工作人员、献血者账号) | 与donor通过donor_id关联 |
| donor | 献血者档案 | 主表 |
| appointment | 预约记录 | 关联donor |
| donation_record | 献血记录(初筛、采血、检测、入库出库) | 关联donor |
| blood_inventory | 血液库存(合格血液袋) | 由donation_record合格后生成 |
| notice | 公告信息 | 无强外键依赖 |
不建议把“性别、血型、民族”做成独立的字典表,因为项目规模不大,用Java枚举或常量类维护就好。如果你为了体现“系统设计”,可以只把“血液成分”和“血液状态”做成字典表。但记住,毕业设计里过度设计比设计不足更危险。
3.2 关键表字段怎么设计
拿两张最核心的表举例。第一张是donor献血者档案表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int 主键自增 | 主键 |
| user_id | int | 关联user表,可为空,允许线下登记 |
| name | varchar(20) | 姓名 |
| id_card | varchar(18) | 身份证号,加唯一索引 |
| gender | varchar(4) | 性别 |
| birthday | date | 出生日期 |
| blood_type | varchar(8) | ABO血型,如A、B、AB、O |
| rh_type | varchar(4) | Rh阴性/阳性 |
| phone | varchar(15) | 手机号 |
| address | varchar(100) | 通讯地址 |
| emergency_contact | varchar(50) | 紧急联系人 |
| emergency_phone | varchar(15) | 紧急联系电话 |
| donation_count | int | 累计献血次数 |
| last_donation_date | date | 最近一次献血日期 |
| status | int | 0正常,1冻结,2注销 |
第二张是donation_record献血记录表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int 主键自增 | 主键 |
| donor_id | int | 关联献血者 |
| appointment_id | int | 关联预约,可为空 |
| staff_id | int | 关联工作人员user_id |
| donation_date | datetime | 采血时间 |
| blood_volume | int | 采血量(单位:ml,常见400/200) |
| physical_status | int | 初筛结果,0未查,1合格,2不合格 |
| test_status | int | 检测结果,0待检,1合格,2不合格 |
| expiry_date | date | 血液有效期至 |
| status | int | 状态:0初筛中,1已采血,2检测中,3合格,4报废,5已入库,6已出库 |
| remark | varchar(200) | 备注 |
这里有一个很容易被忽略的设计细节:donor表里的donation_count和last_donation_date其实可以用SQL从donation_record里统计出来,但我在实际开发时还是选择加上了这两个冗余字段。原因很简单,列表页显示“献血次数”这种高频字段,如果不冗余,每次查询都要COUNT(*),数据量大了以后列表页会明显变慢。这就是典型的“用空间换时间”思想,答辩时可以主动讲出来,反而是个加分点。
3.3 建表SQL示例
建表时我统一使用InnoDB引擎,字符集用utf8mb4,因为utf8mb4才能完整支持中文和一些特殊符号。MySQL 8 + JDBC的配置需要留意驱动类名和时区参数,这个放在部署章节展开。下面给一个donation_record表的参考SQL:
CREATE TABLE donation_record ( id INT AUTO_INCREMENT PRIMARY KEY, donor_id INT NOT NULL, appointment_id INT DEFAULT NULL, staff_id INT DEFAULT NULL, donation_date DATETIME DEFAULT NULL, blood_volume INT DEFAULT 200, physical_status TINYINT DEFAULT 0, test_status TINYINT DEFAULT 0, expiry_date DATE DEFAULT NULL, status TINYINT DEFAULT 0 COMMENT '0初筛中 1已采血 2检测中 3合格 4报废 5已入库 6已出库', remark VARCHAR(200) DEFAULT NULL, FOREIGN KEY (donor_id) REFERENCES donor(id), INDEX idx_donation_status (status), INDEX idx_donation_date (donation_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意几点:donor_id加了外键约束,保证业务数据一致性;status和donation_date都建了普通索引,因为它们被频繁用于条件查询;blood_volume用DEFAULT 200,因为国内常见全血献血量是200ml或400ml,先给一个默认值可以防止漏填。
3.4 为什么用数字做状态而不是直接存中文
这个问题我几乎在每次讲这类项目时都要强调一遍。直接用status VARCHAR(20)存“检测合格”不是不能跑,但写代码的时候会很别扭:你要把中文拿出来做equals判断,一旦页面上文字改成“合格入库”,所有业务代码全部要跟着改。用数字存状态,代码里写if(record.getStatus() == 3)的时候脑子里要清楚3代表什么,这时可以在Java里定义一个常量类:
public class DonationStatus { public static final int SCREENING = 0; // 初筛中 public static final int DRAWN = 1; // 已采血 public static final int TESTING = 2; // 检测中 public static final int QUALIFIED = 3; // 检测合格 public static final int DISCARDED = 4; // 不合格/报废 public static final int IN_STOCK = 5; // 已入库 public static final int OUT_STOCK = 6; // 已出库 }这样所有代码都引用常量,不出现魔法数字,可读性也好了。页面显示的时候再通过JSTL判断数字映射成中文文本,业务层和展示层就彻底分开了。
4. 核心功能实现思路:从登录到统计报表
4.1 登录与基于Session的权限控制
系统的入口是登录。密码存储上,毕业设计至少要做到MD5加密,进阶一点用“MD5+盐”的方式。我不建议在毕业设计里用BCrypt这种复杂加密,不是因为它不好,而是答辩时你很难三两句话讲清楚底层的迭代哈希原理;MD5加盐的思路,老师一听就明白。
权限控制我建议用Filter统一做,而不是在每个JSP页面里用if判断。写一个AuthFilter,在doFilter里从Session取当前用户:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; Object user = req.getSession().getAttribute("loginUser"); String uri = req.getRequestURI(); if (user == null && !uri.endsWith("/login.jsp")) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } // 角色访问范围控制 User u = (User) user; if (u != null) { if (uri.contains("/admin/") && !"ADMIN".equals(u.getRole())) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } if (uri.contains("/staff/") && !("STAFF".equals(u.getRole()) || "ADMIN".equals(u.getRole()))) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } } chain.doFilter(request, response); }这样一个Filter同时完成了“未登录拦截”和“角色越权拦截”。答辩时可以现场演示:用一个献血者账号,直接在地址栏输入的路径访问管理页面,会被403拦截。这个操作几乎每次都能让老师点头。
4.2 献血者档案的分页查询与模糊搜索
档案列表必然要分页。手写分页其实不难:前端传pageNum和keyword,后端算好totalCount、totalPages,查当前页数据。SQL用LIMIT ?, ?,注意offset = (pageNum - 1) * pageSize。
模糊查询的条件拼接是另一个必考细节。用JDBC的情况下,建议全程使用PreparedStatement,不用字符串拼接SQL,这是SQL注入防护的最基本做法。核心DAO层大致长这样:
public List<Donor> searchDonors(String keyword, int pageNum, int pageSize) throws SQLException { List<Donor> list = new ArrayList<>(); String sql = "SELECT * FROM donor WHERE status = 0 AND (name LIKE ? OR phone LIKE ? OR id_card LIKE ?) " + "ORDER BY id DESC LIMIT ?, ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + keyword + "%"); ps.setString(3, "%" + keyword + "%"); ps.setInt(4, (pageNum - 1) * pageSize); ps.setInt(5, pageSize); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Donor d = new Donor(); d.setId(rs.getInt("id")); d.setName(rs.getString("name")); d.setPhone(rs.getString("phone")); // 省略其他字段封装 list.add(d); } } } return list; }还有一个小技巧:查询总条数时用SELECT COUNT(*),但很多人会把总数SQL写得很复杂。其实只需要SELECT COUNT(*) FROM donor WHERE status = 0 AND (name LIKE ? OR phone LIKE ? OR id_card LIKE ?)即可,不需要带排序和分页。分页组件我做了一个简单标签,调用<pg:page totalPage="${totalPage}" pageNum="${pageNum}"/>,也可以直接用JSTL循环生成页码。
4.3 献血登记与库存的事务联动
这个功能是整个系统里最能体现“系统设计”能力的地方。一次完整的献血登记,至少牵涉三张表:donor表要更新donation_count和last_donation_date,donation_record表要插入一条采血记录,如果检测合格且状态流转为“已入库”,blood_inventory表还要插入一条血液库存记录。
这三步操作必须放在同一个数据库事务里,否则就可能出现“献血次数变了但采血记录没插入”之类的脏数据。我在Service层里用Connection手动控制事务,核心逻辑如下:
Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 donorDao.updateDonationCount(conn, donorId); int recordId = donationRecordDao.insert(conn, record); // 模拟检测合格后自动入库 if (record.getTestStatus() == 1) { bloodInventoryDao.insert(conn, buildInventoryFromRecord(record)); } conn.commit(); } catch (Exception e) { if (conn != null) conn.rollback(); throw new RuntimeException("献血登记失败,事务已回滚", e); } finally { if (conn != null) conn.close(); }这串代码看起来不复杂,但它展示了三个关键能力:知道什么时候要开事务、能处理异常回滚、会释放连接资源。把它写进项目里,比单纯做一个“增删改查”的深度要高出不少。
4.4 统计报表:让数据说话
统计报表是给管理员看的,页面建议放在管理员首页。需要做的统计项有:
- 总献血人数、累计献血量、本年献血人次
- 近12个月每月献血人次趋势
- 各血型血液库存量占比
- 各血型献血人群分布
后端思路很简单:写一个统计DAO,用SQL的GROUP BY直接算好,再转成JSON返回。比如近12个月每个月的献血人次,SQL是:
SELECT DATE_FORMAT(donation_date, '%Y-%m') AS month, COUNT(*) AS cnt FROM donation_record WHERE status >= 3 AND donation_date >= DATE_SUB(NOW(), INTERVAL 12 MONTH) GROUP BY month ORDER BY month;前端用ECharts画柱状图和饼图。我会单独写一个/stats/overview.do,返回List<Map<String,Object>>,在JSP里用JSON.toString()转成JSON字符串,塞给ECharts的series.data。注意,如果要防XSS,后端要对字段做转义,但这种统计数字本身没有用户输入内容,风险较小。
4.5 JSP页面的复用与表单回显
JSP页面最容易犯的问题是每个页面复制粘贴头部菜单,后面想改一个链接要全局替换。我建议把头部导航和底部版权信息抽成common/header.jsp和common/footer.jsp,每个页面用:
<%@ include file="/common/header.jsp" %>JSTL和EL表达式在JSP展示层里非常实用。比如遍历献血记录时:
<c:forEach items="${recordList}" var="r"> <tr> <td>${r.donationDate}</td> <td> <c:choose> <c:when test="${r.status == 3}">合格</c:when> <c:when test="${r.status == 4}">报废</c:when> <c:otherwise>处理中</c:otherwise> </c:choose> </td> </tr> </c:forEach>还有一个经常被忽略的细节:表单提交失败后要回显用户已填写的内容。比如工作人员登记献血者时,身份证号填错了,提交后报错,应该把已经填好的姓名、手机号带回页面,而不是清空让用户重新填。做法是在Servlet里把表单对象放回request域:
request.setAttribute("donorForm", form); request.getRequestDispatcher("/staff/registerDonor.jsp").forward(request, response);页面上用${donorForm.name}回填,这样体验会好很多,答辩演示时也更流畅。
5. 部署与联调阶段的高频坑
5.1 Tomcat和JDK版本要匹配
很多同学在本机开发时用的是较新的JDK,然后直接扔到Tomcat里跑,结果各种报错。我先给一个保守稳妥的版本组合,实操中验证过问题最少:
| Tomcat版本 | 推荐JDK | 注意事项 |
|---|---|---|
| Tomcat 8.5 | JDK 8 | 兼容性好,Servlet规范3.1 |
| Tomcat 9 | JDK 8 / 11 | 最常用,Servlet规范4.0 |
| Tomcat 10 | JDK 11+ | 包名改为jakarta,老项目直接部署会报错 |
如果你的项目里用了JSTL标签库,还要注意jstl.jar和standard.jar的版本要与Tomcat匹配。有一种典型错误是Tomcat 10下部署老项目,报java.lang.NoClassDefFoundError: javax/servlet/jsp/tagext/TagLibraryValidator,原因就是Tomcat 10把javax改成了jakarta,而你代码里还是javax。所以我建议直接用Tomcat 9,省去适配问题。
5.2 MySQL 8的驱动类和连接串
早期用MySQL 5.x时驱动类是com.mysql.jdbc.Driver,到了MySQL 8改成了com.mysql.cj.jdbc.Driver,连接串也要多带几个参数,否则中文乱码和时区报错一个接一个:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/blood_donation_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=your_passwordserverTimezone=Asia/Shanghai必须加,不然MySQL 8默认时区和本机不一致,你在代码里new Date()存入数据库后,再取出来发现时间多了8小时或者少了8小时。useSSL=false是为了避免本机开发时一堆SSL证书警告刷屏。
5.3 中文乱码的三层排查顺序
中文乱码是这类JavaWeb项目里最高发的故障。我在项目里按“请求层-服务层-数据库层”三步排查,顺序不能乱:
- 页面本身的编码,JSP文件头部写
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>,保证页面UTF-8。 - 请求编码,在
web.xml里配置一个字符编码过滤器,或者在每个Servlet开头request.setCharacterEncoding("UTF-8"),否则POST提交的中文会乱。 - 数据库连接串加
useUnicode=true&characterEncoding=utf8,建表时用utf8mb4,防止数据写入数据库后变成问号。
如果三层都设了还是乱码,再去检查Tomcat目录下conf/server.xml里Connector是否加了URIEncoding="UTF-8",这个影响GET请求的中文参数,比如搜索关键词。
5.4 端口占用与部署路径问题
本机开发时Tomcat默认8080端口,但经常会有其他程序把8080占了。启动报Port 8080 required by Tomcat v9.0 Server at localhost is already in use,这时要么关掉占用端口的服务,要么把conf/server.xml里的port="8080"改成8081。我给一个查端口占用的小命令(Windows下):netstat -ano | findstr :8080,然后按PID在任务管理器里找进程。Linux/macOS下用lsof -i:8080。
部署路径也要提前想好。如果你打的是war包,Tomcat会自动解压到/webapps/项目名/下,访问路径就是http://localhost:8080/项目名/index.jsp。如果你想直接访问项目根路径不显示项目名,可以修改server.xml的<Context path="/" ...>,但这属于部署优化,不是必须。更简单的做法是在web.xml里把首页指向自己的主页:
<welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list>5.5 演示前必须准备的数据脚本
这个问题很多人直到答辩前才发现:演示当晚,系统里只有一两条数据,页面空空荡荡,统计报表画出来只有一根柱子,尴尬得很。我的建议是:建一个data.sql,里面预先写清楚:
- 三个角色的账号各一个:管理员、工作人员、献血者。
- 至少20~30条献血者档案,名字用“献血者01、献血者02”这类编号,同时包含A/B/O/AB四种血型和Rh阴性样本。
- 至少40条献血记录,分布在不同月份,让统计图表有曲线感。
- 预约记录覆盖“待确认、已确认、已完成、已取消”四种状态,方便演示状态流转。
- 库存里提前插几袋不同血型的合格血液,血量不要全相同,比如O型8袋、A型5袋、B型6袋、AB型2袋。
数据准备这件事,看起来是小活,实际上直接影响答辩效果。你要演示分页,只有3条数据怎么分页?要演示模糊搜索,数据里没有联系方式怎么搜?所以脚本要提前写好,别等到现场手敲。
6. 给答辩加分的三个扩展点与演示顺序
6.1 电子回执与二维码追踪
扩展功能不用多,选一两个做扎实就够了。我见过做得比较成功的方案是给献血者生成电子献血证回执:检测合格后,系统生成一个二维码,二维码内容是一段URL,指向这个献血记录的详情页。工作人员或献血者扫码就能看到采血时间、血量、血型、检测结论,既方便又直观。
技术实现上可以用ZXing库生成二维码,然后以图片流输出:
// 伪代码示意:生成二维码图片输出到response QRCodeWriter writer = new QRCodeWriter(); BitMatrix matrix = writer.encode(content, BarcodeFormat.QR_CODE, 200, 200); MatrixToImageWriter.writeToStream(matrix, "png", response.getOutputStream());这个功能工作量不大,但视觉上非常加分,答辩时能实实在在演示“扫码查看”这个动作,比你口头描述“系统可以查询”有力得多。
6.2 献血间隔智能提醒
我国规定全血献血间隔不少于6个月。这个规则可以用在预约环节:工作人员为某位献血者登记预约时,系统自动判断last_donation_date距今是否满6个月,如果不满,自动弹出提示“该献血者距离上次献血仅X个月,不符合间隔要求”。
后端判断逻辑很简单,用Java的LocalDate计算两个日期之间的月份差:
long months = ChronoUnit.MONTHS.between(lastDonationDate, LocalDate.now()); if (months < 6) { // 提示并阻止登记 }把这个规则写进预约审核功能里的意义在于:它让系统从“记录数据的工具”变成了“有业务规则的系统”。答辩时老师问“为什么设计这个功能”,你可以回答“这是为了避免因为违反献血间隔而影响献血者健康”,这个回答比“因为题目需要”强太多。
6.3 献血结果合格率统计图
统计页不要只做“每月人数”一个图表,我建议加一个“血液检测合格率”的环形图,展示合格样本和报废样本的比例。这个数据非常直观地反映采血点的工作质量,概念上也比单纯的“总人次”更有业务深度。算合格率的SQL:
SELECT SUM(CASE WHEN test_status = 1 THEN 1 ELSE 0 END) AS qualified, SUM(CASE WHEN test_status = 2 THEN 1 ELSE 0 END) AS unqualified, COUNT(*) AS total FROM donation_record WHERE status >= 2;图表的展示形式可以是饼图,也可以是两个数字卡片加一个进度条,后者实现更简单,视觉效果也不差。如果做得细致一点,可以按血型分组展示各血型的合格率,这在血液库存管理中是有实际意义的。
6.4 一套完整的演示流程参考
最后,我想给一套实际操作过的演示顺序,按这个顺序走下来,整个系统看起来会非常完整,老师挑不出大毛病:
- 先用三个角色分别登录,展示不同角色的首页和菜单差异。
- 以工作人员身份新增一位献血者档案,演示表单校验和提交回填。
- 演示分页、搜索、修改、删除(逻辑删除)等基础CRUD操作。
- 给这位献血者新建预约,然后走“到场初筛-采血-检测合格-入库”的完整状态流转。
- 到库存页面,看到刚才合格入库的血液出现在库存列表中,血型和血量都正确。
- 到统计报表页,看到趋势图和血型分布图,数据是真实的动态变化。
- 切换回献血者账号,查看本人的献血记录和电子回执二维码。
- 用特权演示:用一个普通献血者账号访问
/admin/index.jsp,被Filter拦截,说明权限控制不是摆设。
这八步每一步都对应系统里一个明确的模块。整个过程大概5到8分钟,已经能把系统的完整度讲得很透。如果老师追问某个功能的实现细节,你就按这篇里提到的“事务、状态、权限”三个维度去展开回答,基本都能接得住。
最后再分享我的一点个人体会:毕业设计做这类管理系统,最大的误区是以为功能越多越好。实际经验是,做完一个完整的状态流转闭环,比做五个半成品界面要有价值得多。把“预约—初筛—采血—检测—入库—出库”这条链路跑通,把权限和事务这两个点讲清楚,这个项目已经超出了一大半同题目的完成质量。如果想在这个基础上再进一步,可以往预约消息提醒和献血者历史趋势分析的方向扩展,但那是后话了,先把当前系统做稳做透更重要。