先说结论:这个项目是个典型的医疗信息化管理系统,技术栈是Node.js + Vue,核心场景从标题就能拆出来——血液中心、血库、献血、管理。简单说就是给血站、血液中心或医院输血科做的一套前后端分离系统,管的是从“有人来献血”到“血液进库”再到“临床用血出库”的全流程。这个方向现在挺值得做的,医疗行业信息化一直在推进,中小型血站和医院的信息化程度参差不齐,一套够用、能跑、可定制的血库管理系统,在很长一段时间内都有实际需求。
我做完这个项目之后最大的感受是:这类系统技术难度不算高,真正的难点全在业务流程上。血液不是普通商品,它有效期、有血型、有严格的检测流程和追溯要求,每袋血从采到用、从入库到报废,每一步都得能追溯到源头。所以写这套系统,技术选型只是表面,把业务规则吃透才是核心。我就围绕这个项目,从设计思路、技术选型、核心功能实现到坑点排查,完整拆一遍,给想做同类系统的朋友一个能直接参考的落地模板。
1. 项目拆解与技术选型
1.1 系统边界:不只是“记录献血”那么简单
很多人看到“献血管理系统”就以为只是登记一下献血人信息,实际远不止。血库业务是一个完整的链条,粗略分有六大块:
- 献血者管理:登记基本信息、身份证校验、既往献血记录、体检指标(体重、血压、血红蛋白等)、不宜献血名单管理。
- 血液采集与检测:采血登记、血袋条码生成、标本送检、检测结果回填(乙肝、丙肝、艾滋、梅毒、ALT转氨酶等)。
- 血液库存管理:合格血液入库、库存查询、效期管理(全血和红细胞通常是35天,血小板只有5天)、血型分布统计。
- 血液出库管理:临床用血申请、审核、发血登记、退血处理。
- 报废管理:过期报废、检测不合格报废、报废原因统计。
- 统计报表:采血量、用血量、库存量、报废率、献血人次等核心指标的可视化。
这套系统最核心的价值是解决两个问题:一是每袋血的全流程追溯,扫码能查到这袋血是谁献的、什么时候采的、检测结果是什么、现在存哪个冰箱、谁发出去的;二是库存预警,哪种血型快不够用了、哪些血液快过期了,系统主动提醒,而不是等临床用血时才发现不够。
1.2 为什么选 Node.js + Vue,而不是 Java 或传统后端
做技术选型的时候,我身边有同事建议用 Spring Boot + Vue,因为医疗系统里的老牌产品大多是 Java 系。但我最终选了Node.js(Express) + Vue 3,基于几个很实际的考量:
第一,交付效率。这个项目需要快速出可用版本,Node.js 的开发链路短,从接口定义到联调测试整个节奏快。同一套 JSON 数据结构前后端都能看,沟通成本低。如果你自己做过 Java + Vue 的分离开发,应该懂那种“前端等后端接口、后端等前端联调”的时间损耗。Node.js 下前后端都可以一个人写,一个人干两个人的活。
第二,I/O 密集型场景匹配。血库系统虽然数据量不大,但操作频繁——每个环节都涉及查询、登记、审核,这些操作大都是轻量级的 I/O 操作,正好是 Node.js 的强项。真正的痛点不在计算性能,而在业务流程的完整性,Node.js 完全可以扛住。
第三,生态成熟度。Express 中间件、mysql2 驱动、JWT 鉴权、Vue 3 全家桶,这些方案在社区里被验证过无数次,遇到问题几乎都能搜到现成解决方案,不会卡在某个冷门坑里。
提示:不是说 Spring Boot 不好,如果是给三甲医院核心系统配套,那 Java 系的稳定性和生态确实更适合。但如果是血站单独部署的系统、区县级血液中心的管理平台、或者作为课程设计和项目实践,Node.js 这套组合的性价比明显更高。
1.3 前后端分离架构怎么切分
整体架构就是标准的前后端分离:
vue-admin-frontend/ # Vue 3 + Vite + Element Plus node-server/ # Express + mysql2 + JWT ├── routes/ # 路由层:按业务模块拆分 ├── controllers/ # 控制器:处理业务逻辑 ├── services/ # 服务层:复杂业务封装 ├── models/ # 数据模型:SQL语句封装 ├── middleware/ # 中间件:鉴权、日志、异常处理 └── utils/ # 工具函数后端按业务模块拆路由:/api/donors、/api/blood-stock、/api/collection、/api/issuance、/api/stats、/api/users。前端按页面拆视图:登录页、工作台仪表盘、献血者管理页、库存管理页、出入库登记页、报表页面。前后端通过 RESTful API 通信,身份认证用 JWT token。
这个架构最明显的好处是业务边界清楚。血库系统里“血液状态流转”特别重要:一袋血从“已采集”到“检测中”再到“合格入库”或“报废”,状态一变,相关页面的按钮可用性、数据显示方式就要跟着变。前后端分离后,这些状态通过接口返回,前端做展示控制,后端做流程校验,职责分明。
2. 数据库设计与核心业务模型
数据设计是这类系统的灵魂,直接关系到后面所有功能能不能顺利落地。我用了 MySQL 8.0,原因很简单:血库系统对数据一致性要求高,事务性操作多(比如出库时要同时扣库存、生成发血记录、更新血袋状态),关系型数据库处理这种场景比 NoSQL 靠谱得多。
2.1 核心表结构设计
我把表分成三组:基础信息表、业务过程表、系统管理表。挑几张核心的表讲。
献血者表(donors)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint 主键 | 自增 |
| id_card | varchar(18) | 身份证号,唯一索引 |
| name | varchar(50) | 姓名 |
| gender | tinyint | 1男 2女 3未知 |
| birth_date | date | 出生日期 |
| blood_type | varchar(10) | ABO血型,允许空 |
| rh_type | varchar(10) | RH阳性/阴性 |
| phone | varchar(20) | 联系电话 |
| address | varchar(255) | 住址 |
| total_donations | int | 累计献血次数 |
| last_donation_date | date | 最近献血日期 |
| status | tinyint | 1正常 2暂缓 3永久不宜 |
注意blood_type和rh_type为什么不合并成一个字段?因为实际查询里经常要按 ABO 和 RH 分别筛选,拆开建索引效率更高,而且业务语义清楚。
血液库存表(blood_stock)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint 主键 | 自增 |
| bag_code | varchar(50) | 血袋条码,唯一索引 |
| blood_type | varchar(10) | ABO血型 |
| rh_type | varchar(10) | RH阳性/阴性 |
| component_type | varchar(20) | 成分类型:全血/红细胞/血浆/血小板 |
| volume_ml | int | 容量(毫升) |
| donor_id | bigint | 关联献血者 |
| collection_date | datetime | 采集日期 |
| expiry_date | datetime | 有效期至 |
| storage_location | varchar(50) | 存储位置(如冷藏库A-3) |
| status | tinyint | 1待检 2检测中 3合格在库 4已出库 5报废 6退血回库 |
| create_time | datetime | 入库时间 |
bag_code是整条追溯链的主线索,从采血登记时生成,到出库发血时扫码验证,全靠它串联。
献血登记表(donation_records):记录每次采血明细,字段包括关联献血者 id、采血日期、采血护士、血量、血袋条码、检测状态、检测结果备注。
用血申请与发血记录表(issuance_records):记录临床用血申请、审核人、发血人、领血人、用血科室、出库血量、关联的血袋条码。
2.2 血液状态流转:这是业务逻辑的核心
整个系统最关键的规则就是血液状态机。一袋血的完整生命周期:
已采集(1) → 标本送检(2) → 检测合格 → 合格入库(3) → 检测不合格 → 报废(5) 入库后 → 临床用血申请 → 审核通过 → 发血出库(4) 入库后 → 超过有效期 → 报废(5) 已出库 → 临床退回 → 退血回库(6) → 再次入库(3) 或 报废(5)这个状态机不是只存在于代码注释里,而是要落实到后端接口的每个修改操作上。比如只有 status=3 的血液才能出库,前端按钮要根据状态禁用,后端接口也要校验,不能只靠前端控制。这个状态流转我用了一张状态转换表做了硬约束,后端在接收状态变更请求时校验当前状态是否允许变化。这一步在实际项目中极其重要,不然很容易出现“把已报废的血液又发出去”这种事故级 bug。
2.3 库存预警模型:早做设计,后面省大事
库存预警是血库系统的高频使用功能,我把它做成了一张独立的预警规则表(stock_alerts),而不是写死在代码里:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| blood_type | varchar | ABO血型,或用 0 代表所有 |
| rh_type | varchar | RH类型 |
| component_type | varchar | 成分类型 |
| min_quantity | int | 最低库存(袋) |
| max_quantity | int | 最高库存(袋) |
| expiry_warn_days | int | 效期预警天数 |
预警计算逻辑:每天定时任务跑一次,统计每种血型×成分的在库数量,低于min_quantity触发库存低预警;高于max_quantity触发库存高预警;库存里存在expiry_date - 当前时间 < expiry_warn_days的血袋则触发效期预警。这个设计的好处是运营人员可以直接在界面上调整不同血型的预警线,不需要改代码重新部署。
3. Node.js 后端搭建与关键接口实现
3.1 初始化项目与环境配置
这一节我踩过的坑比写业务代码还多,值得先讲清楚。首先是 Node.js 的本机环境问题——很多朋友在新建项目第一步就被卡住了,最常见的报错是:
npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本这个报错的本质是 PowerShell 的执行策略默认不允许运行 .ps1 脚本。解决办法有两个:一是在终端执行Set-ExecutionPolicy RemoteSigned修改执行策略;二是用管理员身份打开 PowerShell。我更推荐方案一,因为只需要改当前用户的执行策略,改了之后 npm 命令就能正常用了。改完记得验证一下node -v和npm -v,确认安装成功。
创建项目的命令我建议按这套流程走:
# 创建项目目录 mkdir node-server && cd node-server # 初始化 package.json npm init -y # 安装 Express 和核心依赖 npm install express mysql2 cors jsonwebtoken bcryptjs dotenv dayjs # 安装开发依赖(热更新) npm install -D nodemon这里给几个版本建议:Node.js 用 18 以上版本,Express 用 4.x(5.x 虽然已经稳定,但社区生态还是 4.x 更成熟),mysql2 是必须的(原生 mysql 包在处理预处理语句方面不如 mysql2),bcryptjs 用纯 JS 实现的版本,避免 node-gyp 在 Windows 上编译报错。
3.2 数据库连接与中间件设计
数据库连接我用连接池方式,核心配置文件db.js:
const mysql = require("mysql2"); const pool = mysql.createPool({ host: process.env.DB_HOST || "localhost", port: process.env.DB_PORT || 3306, user: process.env.DB_USER || "root", password: process.env.DB_PASSWORD || "", database: process.env.DB_NAME || "blood_bank", waitForConnections: true, connectionLimit: 10, namedPlaceholders: true, }); module.exports = pool.promise();pool.promise()这个写法很关键,它返回 Promise 包装的查询对象,可以直接用await语法,不用再手写 Promise 包装。namedPlaceholders: true开启后,SQL 里可以写:status这种占位符,可读性比?强很多。
中间件方面,我认为最重要的三个:
- JWT 鉴权中间件:除了登录接口,其余所有
/api接口都走它,验证 token 解析出的 userId 是否合法。 - 全局错误处理中间件:所有
try/catch里的next(error)统一在这里处理,生成格式统一的错误响应。这样接口层代码会非常干净,不用每个路由里都重复写res.status(500).json(...)。 - 请求日志中间件:记录请求方法、路径、耗时、用户 id,方便排查问题。
3.3 核心接口实现:库存查询与发血出库
先说库存查询接口,这是高频接口。实际需求是按血型、RH、成分、状态组合筛选,同时要分页。用mysql2的动态 SQL 拼接:
// controllers/stockController.js exports.listStock = async (req, res, next) => { try { const { bloodType, rhType, componentType, status, page = 1, pageSize = 10 } = req.query; const offset = (Number(page) - 1) * Number(pageSize); const conditions = []; const params = {}; if (bloodType) { conditions.push("blood_type = :bloodType"); params.bloodType = bloodType; } if (rhType) { conditions.push("rh_type = :rhType"); params.rhType = rhType; } if (componentType) { conditions.push("component_type = :componentType"); params.componentType = componentType; } if (status) { conditions.push("status = :status"); params.status = status; } const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""; const [rows] = await req.pool.query( `SELECT * FROM blood_stock ${where} ORDER BY expiry_date ASC LIMIT :offset, :pageSize`, { ...params, offset, pageSize } ); const [[{ total }]] = await req.pool.query( `SELECT COUNT(*) AS total FROM blood_stock ${where}`, params ); res.json({ code: 0, data: { list: rows, total, page: Number(page), pageSize: Number(pageSize) } }); } catch (error) { next(error); } };这里做了一个有意为之的设计:按 expiry_date 升序排序,这是血库“先进先出”原则的体现。发血的时候如果存在多袋同型血液,系统默认优先发效期最近的,减少过期报废。
发血出库接口是最需要注意事务控制的接口:
exports.issueBlood = async (req, res, next) => { const conn = await req.pool.getConnection(); try { const { bagCode, recipientHospital, recipientDept, applicantDoctor, patientName } = req.body; await conn.beginTransaction(); // 1. 锁定血袋记录,查询当前状态 const [rows] = await conn.query( "SELECT * FROM blood_stock WHERE bag_code = :bagCode FOR UPDATE", { bagCode } ); if (rows.length === 0) { await conn.rollback(); return res.status(404).json({ code: 404, message: "血袋不存在" }); } const stock = rows[0]; if (stock.status !== 3) { await conn.rollback(); return res.status(400).json({ code: 400, message: `当前血袋状态不允许出库(当前状态:${stock.status})` }); } // 2. 更新血袋状态 await conn.query( "UPDATE blood_stock SET status = 4, issuer_id = :userId WHERE id = :id", { userId: req.user.id, id: stock.id } ); // 3. 插入发血记录 await conn.query( `INSERT INTO issuance_records (bag_code, blood_type, rh_type, volume_ml, recipient_hospital, recipient_dept, applicant_doctor, patient_name, issue_user_id, issue_time) VALUES (:bagCode, :bloodType, :rhType, :volumeMl, :recipientHospital, :recipientDept, :applicantDoctor, :patientName, :issueUserId, NOW())`, { bagCode: stock.bag_code, bloodType: stock.blood_type, rhType: stock.rh_type, volumeMl: stock.volume_ml, recipientHospital, recipientDept, applicantDoctor, patientName, issueUserId: req.user.id, } ); await conn.commit(); res.json({ code: 0, message: "发血成功", data: { bagCode } }); } catch (error) { await conn.rollback(); next(error); } finally { conn.release(); } };这里三个关键点:
FOR UPDATE行级锁:防止两个并发请求同时出库同一袋血,这在血库系统里可不是理论问题——两个科室同时申请同一袋血,如果没有锁就可能造成数据错乱。- 先查后更:查状态、校验状态、更新状态三步必须在一个事务里,中间任何一个失败都回滚。
req.user.id从 JWT 中间件取:发血人信息不需要前端传,前端传的 user id 不可信,必须从鉴权中间件解析出来。
4. Vue 前端开发与核心页面落地
4.1 Vue 3 项目初始化与工程化配置
前端我基于 Vue 3 + Vite + Element Plus + Pinia 搭建,用 Vite 新建项目的命令:
npm create vite@latest vue-admin-frontend -- --template vue cd vue-admin-frontend npm install npm install element-plus pinia vue-router axios安装完成后要做的第一件事不是写页面,而是配置开发环境代理,解决前后端联调的跨域问题。在vite.config.js里:
import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { "/api": { target: "http://localhost:3000", changeOrigin: true, }, }, }, });这个配置会让前端所有以/api开头的请求都转发到http://localhost:3000,前端代码里直接用相对路径/api/donors/list写接口地址就行,开发时不会遇到跨域报错。生产部署时,前端构建出来的静态文件放在 Nginx 下,Nginx 再反向代理/api到 Node 服务,模式是一模一样的。
4.2 路由设计:动态权限和菜单
这个系统的角色我设计了三种:系统管理员(管用户、管参数配置)、血库工作人员(管库存、管出入库登记)、查询统计人员(只看报表和统计)。不同角色能看到的菜单不同。用 Vue Router 的addRoute做动态路由,登录成功后,根据后端返回的权限码列表,动态注册页面路由:
// router/index.js import { createRouter, createWebHistory } from "vue-router"; const router = createRouter({ history: createWebHistory(), routes: [ { path: "/login", component: () => import("@/views/Login.vue") }, { path: "/", component: () => import("@/layouts/MainLayout.vue"), redirect: "/dashboard", children: [], }, ], }); export function setupDynamicRoutes(permissionCodes) { const allRoutes = []; if (permissionCodes.includes("dashboard")) { allRoutes.push({ path: "dashboard", name: "工作台", component: () => import("@/views/Dashboard.vue") }); } if (permissionCodes.includes("donor:view")) { allRoutes.push({ path: "donors", name: "献血者管理", component: () => import("@/views/DonorList.vue") }); } if (permissionCodes.includes("stock:view")) { allRoutes.push({ path: "stock", name: "血液库存", component: () => import("@/views/StockList.vue") }); } allRoutes.forEach((route) => router.addRoute("/", route)); }路由懒加载用了动态import(),这样打包后每个页面单独一个 chunk,首屏加载速度会快很多。这也是一个真实项目的基本操作,不要一个 app.js 好几兆直接塞给用户。
4.3 核心页面的实现:库存看板和献血登记表单
库存看板页是全系统使用频率最高的页面,我把它设计成三部分:
- 上部分是统计卡片:按血型显示当前在库袋数,红黄绿色标识库存预警状态(绿色正常、黄色偏低、红色极低)。
- 中间部分是筛选区:血型、RH、成分、状态的筛选下拉框。
- 下部分是库存表格:展示每袋血的条码、血型、效期、位置、状态,操作列提供“出库登记”按钮。
这个页面的逻辑不算复杂,但有个细节值得提:表格的刷新策略。出库操作后,不能只修改本地数据,而是重新调用接口拉最新列表。血液库存是多人协作的系统,另外一个人可能刚操作过,本地缓存不可信。我在出库成功后加了个loadList()的重新拉取,体验会略慢一点,但数据绝对准。
献血登记表单是整个系统数据录入量最大的页面。字段包括献血者基本信息(身份证、姓名、性别、出生日期、电话)、本次献血信息(采血日期、采血量、血型初筛)、采血人员。身份证号可以自动校验位数和地区码,血型框用下拉选择并带“待检”选项,因为现场初筛血型不一定准,最终血型以检验结果为准。表单校验用的是 Element Plus 自带的表单校验规则,身份证和电话用正则。
const rules = { idCard: [ { required: true, message: "请输入身份证号", trigger: "blur" }, { pattern: /(^\d{15}$)|(^\d{17}([0-9]|X)$)/, message: "身份证号格式不正确", trigger: "blur" }, ], phone: [ { pattern: /^1[3-9]\d{9}$/, message: "手机号格式不正确", trigger: "blur" }, ], volumeMl: [{ required: true, message: "请选择献血量", trigger: "change" }], };表单提交后调/api/collection/register接口,后端会做整个登记流程:创建或更新献血者档案、生成血袋条码、创建献血记录、生成待检血袋。前端拿到成功提示后清空表单,重置数据。
4.4 状态可视化:让“一袋血的一生”看得见
这个功能是我后期加上去的,但加了之后业务方好评度非常高。在库存表格里点某袋血的“履历”,会弹出一个时间轴组件,展示这袋血从采集、送检、入库、出库的每一步记录:
<template> <el-timeline> <el-timeline-item v-for="(item, index) in historyList" :key="index" :timestamp="item.operateTime" > {{ item.operateDesc }} <p>操作人:{{ item.operatorName }}</p> </el-timeline-item> </el-timeline> </template>这个功能的实现逻辑很简单——建一张blood_trace_log表,每次状态变更都写一条记录,字段就是bag_code、operate_type、operate_desc、operator_id、operate_time。前端问接口拉出来渲染成时间轴就行。
但关键价值在于:它逼迫我在设计后端时保持操作都有记录。写日志这件事前期容易被忽略,后期补起来特别痛苦——因为历史操作已经找不回来了。所以我也建议做同类系统的人,数据库设计阶段就把 trace_log 留下,哪怕简单几个字段,一定不要省。
5. 联调、部署与常见问题排查
5.1 前端联调阶段最容易踩的坑
跨域问题。虽然配置了 Vite proxy,但有时候改了配置不生效,原因通常是 Vite 需要重启。改vite.config.js之后,开发服务器不会热更新配置,必须Ctrl+C停掉再npm run dev重启。这个坑我反复踩过,后来养成了习惯:改完配置一律重启,别等报错才反应过来。
表单提交后不刷新页面。这个不是技术问题,是交互设计问题。提交成功后,我一开始只弹了个成功提示,没重置表单也没刷新表格,用户连续录入第二个人的信息时,上一单的数据还残留着,很容易误提交。后来改成了提交成功 →ElMessage.success→ 重置表单 → 刷新列表,一步到位。
日期格式问题。后端返回的时间字段默认是 ISO 格式,类似2025-03-01T15:30:00.000Z,直接显示在页面上非常不友好。前端统一封装了一个 utils/format.js 格式化函数,用 dayjs 转成YYYY-MM-DD HH:mm:ss。别在组件里散落地写格式化逻辑,统一封装,以后如果要改格式,改一个文件就行。
npm.ps1 报错。这个在合作同学的新电脑上又出现了一次,现在基本成了 Node.js 环境标配问题。再次强调:运行Set-ExecutionPolicy RemoteSigned -Scope CurrentUser解决,比网上说的改注册表什么的都干净。
5.2 生产环境部署:Nginx 静态资源 + Node 服务
生产部署我是这样做的:
- 前端打包:
npm run build,产出的dist/目录丢到服务器 Nginx 的静态目录,比如/var/www/blood-bank-web/。 - Node 后端:用
pm2跑,pm2 start server.js --name blood-bank-server,端口监听127.0.0.1:3000,不对公网直接暴露。 - Nginx 配置核心片段:
server { listen 80; server_name blood.example.com; root /var/www/blood-bank-web; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files这行一定要写,因为 Vue Router 是 history 模式,用户刷新/stock页面时,Nginx 先把请求映射到物理文件,找不到就退回index.html,由前端路由接管。
pm2值得多说两句。Node 服务直接node server.js跑在服务器上,SSH 断开进程就没了。用 pm2 的好处是守护进程、崩溃自动重启、日志文件管理。几个常用命令要记:pm2 start、pm2 logs、pm2 restart、pm2 save。开机自启用pm2 startup生成系统服务,然后pm2 save保存当前进程列表。
5.3 线上真实存在的几个问题
问题一:并发出库同一袋血,导致库存数据不一致
这就是上面FOR UPDATE行级锁要解决的问题。线上发生过两个科室同时申请同一袋血,两边都点“确认发血”,结果两条发血记录指向同一个血袋条码。加了事务锁之后,第二个请求会等待第一个提交,提交完再查状态发现已出库,直接返回“当前状态不允许出库”。这个问题的严重性不用我说,在血库系统里属于重大事故隐患。
问题二:库存总量对不上,三张表数据矛盾
排查了很久,最后发现是操作逻辑跳过了状态机校验。比如退血回库的功能,前端直接调用了更新接口把 status 从 4 改成 3,但没插入 trace_log 也没更新 issuance_records 里的退血字段。原因是我后端偷懒用了同一个通用更新接口,没有针对“退血”这个业务单独写接口。后来我补了专门的退血接口,把状态更新、日志写入、退血信息登记都封装在一个事务里。
问题三:报表统计口径不一致
统计采血量的时候,一开始直接SELECT SUM(volume_ml) FROM donation_records,但这里有个坑——采集的血不等于合格入库的血,中间会有检测不合格的报废量。业务方要的“采血量”统计口径有两种,后来我在接口里加了statType参数,让前端切换是按采集口径还是按入库口径统计,避免业务扯皮。
6. 系统扩展与二次开发建议
6.1 二维码扫码功能:血袋追溯的硬件支持
系统上线后收到最多的需求就是扫码。血站采血时会把条码贴在血袋上,后续每个环节都应该扫码。如果要扩展扫码能力,考虑到血站工作人员用的都是手持 PDA,最好对接HTML5+扫码(适用于 HBuilder 打包的 App),或者直接用带扫码枪的 PC。前端扫到条码之后,调后端接口/api/blood-stock/:bagCode查询详情,数据基本不用重新开发。
6.2 对接第三方检测设备或者 LIS 系统
血站通常有专门检验科,血液标本要送到检验科用大型生化仪、酶免仪检测,检测结果通常存在 LIS(实验室信息系统)里。如果要求对接 LIS,通常方案是后端写定时任务,按时间段拉取 LIS 开放的接口数据,回填到donation_records的检测结果字段,然后自动把检测合格的血液状态从“检测中”改为“合格入库”。这套对接的核心是数据映射:LIS 返回的样本号要能和我们系统的血袋条码对应,LIS 返回的项目编码要能映射到我们系统的字段。
6.3 消息通知机制:库存预警推给对应的人
现在库存预警是登录系统后在首页看到,但工作人员不可能天天盯着屏幕。扩展方向是加企业微信或者钉钉的 webhook 机器人通知,定时任务发现低于库存下限时,往血液中心的工作群里推一条:“A型红细胞库存不足,当前仅 3 袋,低于预警线 10 袋”。这个功能实现成本很低,webhook 就是一个 HTTP POST 请求,Node 后端一天就能接完,但实用价值非常大。
6.4 数据大屏可视化
血站领导喜欢看“数据大屏”,办公室墙上挂一块屏,展示今日采血量、本周用血趋势、各血型库存占比、报废率这些核心指标。技术方案用 ECharts 或 DataV,数据接口直接用现有统计接口,前端做一个大屏专用路由,不需要动后端。如果要做这个事,建议数据接口尽早支持“按日、按周、按月”的时间维度参数,不然到时候前端要改接口就麻烦。
7. 项目收尾的实际体会与建议
做完整套系统回头看,这个项目最难的环节根本不是写代码——代码就是那些增删改查,Vue 和 Express 的语法都有官方文档,遇到报错一搜就有答案。真正让这个项目有价值的地方,是我花了很大精力把血库的业务规则搞清楚,并且让这些规则切实落到代码逻辑里:血液状态不能乱跳、库存数据必须用事务保证一致、每一步操作都留下可追溯的日志、过期血液系统要及时给出预警。这些业务逻辑才是血库管理系统区别于一个普通“信息登记系统”的关键。
如果要给准备做同类系统的人一个最核心的建议,我会说:动手写代码之前,先画业务流程图和状态机。把“血液从进到出要经历哪些状态、每个状态允许什么操作、操作后数据怎么变化”全部画出来,想清楚再敲代码。这一半的时间花在前期设计上,后面开发和测试的时间会节省一半以上。
另外在技术深度上,这套系统的核心难点在于“事务和数据一致性”,能把 MySQL 事务、行级锁、动态 SQL 查询这些基本功练扎实,这个项目就没白做。