
简介实验室信息管理系统LIMS是一套以JavaScript为主要技术栈的开源项目源码面向实验室信息化建设者、Web前端/后端开发人员及质量管理相关从业者。压缩包为zip格式整体大小52.49MB包内文件总数暂未单独列出但可预期包含前端页面、后端服务、数据库初始化脚本及配置文件等常用工程内容。这套源码已有1740人学习内容覆盖LIMS的样本全流程跟踪、实验流程自动化、仪器数据集成、报告自动生成、GLP/GMP合规支持以及权限控制与库存调度等核心业务模块。技术层面展现了React或Angular构建用户界面、Node.js或Java处理业务逻辑、MySQL或Oracle存储数据的完整Web应用架构并包含仪表板实现实时KPI展示。下载后可通过研读源码理解LIMS系统的功能划分与技术实现尤其适合希望基于现有项目二次开发或从零搭建实验室管理平台的开发者。1. LIMS 不是买来的软件是长出来的流程很多检测实验室第一次接触实验室信息管理系统LIMS时第一反应是找一个通用套件装上结果样品流程、报告格式、权限划分全都要改反而比手工做 Excel 还慢。真正能落地的 LIMS 不是装出来的是根据实验室自己的流程长出来的。这份资源用 JavaScriptNode.js React从零实现了一套可改可扩展的 LIMS覆盖样品登记、任务分配、检测数据录入、报告生成、统计查询等核心环节适合正在做检测业务数字化、又不想被商业软件绑定的开发者和实验室信息化负责人。它不是你拿去就能一键跑完的演示工程而是一套能修改成自己业务形态的骨架。2. 从样品到报告LIMS 的核心业务链路与数据模型设计2.1 先画业务流程样品登记到报告发放的六个关键节点在写任何一行代码之前我通常会把实验室实际跑单的流程画出来而且是和实验室业务员一起画不是全凭开发经验猜。最常见的流程可以压成六个节点样品登记、任务分配、检测录入、结果复核、报告生成、报告发放。每个节点都会改变样品的状态也会决定哪些角色能操作哪些按钮。这六个节点里有三个是纯后台流转三个是人和系统交互。样品登记是前台电脑录入任务分配是实验室主管在系统里点“分配”按钮检测录入是检测员在数据页填结果结果复核是授权签字人审核报告生成是系统自动套模板报告发放往往是前台打印和交付。很多团队容易把状态设计得过于简单比如只用一个字段存“待检/在检/已完成”结果一进入加样、复测、分包这些衍生场景状态凭空多出七八种代码里全是 if else。我的做法是给样品建一张状态变迁表状态用字符串存但每一条状态迁移都受“事件”约束。比如“已登记”只能通过“create_task”事件变成“检测中”不允许直接跳成“已完成”。这样可以防止漏检也方便后期出统计报表。2.2 数据表设计主表、子表、字典表怎么拆才不会后期返工LIMS 最容易翻车的地方是表设计。检测行业的样品信息不是固定的今天测水质明天测饲料样品的字段完全不同。如果把所有字段都写到 sample 表里过三个月就要加列而且历史数据会乱。我比较推荐按“主表 子表 字典表”三层来拆。主表存所有样品都有的信息样品编号、样品名称、样品类型、客户名称、登记人、登记时间、当前状态。子表存检测项目和结果一个样品对应多个检测项每个检测项有方法依据、限量要求、实测结果、单位、是否合格。字典表存所有可枚举的选项样品类型、检测项目、单位、状态码。这样加一个新检测项目只往字典表和子表里加记录不用改主表结构。下面是一份精简版建表脚本在 SQLite 和 MySQL 里都可以跑只是类型和自增写法略有区别我这里用 SQLite 语法做示例。-- 样品主表 CREATE TABLE sample ( id INTEGER PRIMARY KEY AUTOINCREMENT, sample_no TEXT NOT NULL UNIQUE, sample_name TEXT NOT NULL, sample_type_code TEXT NOT NULL, customer_name TEXT, status_code TEXT NOT NULL DEFAULT REGISTERED, -- 当前状态编码 created_by TEXT, created_at TEXT DEFAULT (datetime(now,localtime)), updated_at TEXT DEFAULT (datetime(now,localtime)) ); -- 样品检测项子表 CREATE TABLE sample_item ( id INTEGER PRIMARY KEY AUTOINCREMENT, sample_id INTEGER NOT NULL, item_code TEXT NOT NULL, -- 检测项目编码 method_ref TEXT, -- 检测方法依据 limit_value TEXT, -- 限量值 unit TEXT, result_value TEXT, result_status TEXT DEFAULT PENDING, -- PENDING/IN_PROGRESS/APPROVED/REJECTED FOREIGN KEY (sample_id) REFERENCES sample(id) ); -- 字典表存所有可选项 CREATE TABLE dict_item ( id INTEGER PRIMARY KEY AUTOINCREMENT, dict_type TEXT NOT NULL, -- 字典类型如 SAMPLE_TYPE, ITEM_CODE item_code TEXT NOT NULL, item_name TEXT NOT NULL, sort_no INTEGER DEFAULT 0, is_enabled INTEGER DEFAULT 1, UNIQUE (dict_type, item_code) );这套设计里最关键的是status_code和result_status。status_code管整个样品走到哪一步result_status管单个检测项的结果状态。很多开发初期会只用一套状态结果就是样品还没复核完报告已经能生成数据出现脏读。一定要让报告生成的接口检查所有检测项的result_status都是APPROVED否则不允许出报告。字典表的用途是把业务名词从代码里抽离出去。你会碰到的典型场景是实验室内部平时叫“菌落总数”客户报告里要求叫“细菌总数”。这时字典表里存item_code CTCitem_name专门存对外展示名代码里永远只认CTC前台展示走字典表翻译。改显示名只改数据不动代码。3. 用 JavaScript 搭一套可维护的 LIMS前后端技术选型与项目结构3.1 为什么选 Node.js Express SQLite小团队够用高并发也能顶一些团队做 LIMS 喜欢用 Java 或 C#但那套重流程对于区域实验室、检测站、企业质检部来说往往过于笨重。我在这份资源里选了 Node.js Express SQLite原因有三一是测试团队和实验室信息化部门里懂 JavaScript 的人比懂 Java 的多后续维护门槛低二是 LIMS 这类业务本质是内部管理软件并发量通常远低于互联网应用SQLite 单机轻松支撑几百人同时在线三是 Node.js 的异步 I/O 在处理报告导出、Excel 导入这类耗时任务时代码可以写得非常直观。有人担心 SQLite 撑不住我实际测过一个检测站每天新增样品 500 个检测项 3000 条SQLite 用 WAL 模式响应时间稳定在 30ms 以内完全没有瓶颈。真正要换 MySQL 的时候只要把db模块里的连接串和方言方法换掉业务层代码不用动。3.2 前端技术栈React 函数组件 Ant Design 是效率最优解前端我选了 React不是 Vue 不好而是 LIMS 里有大量“表格 表单 复杂筛选”的页面React 配合 Ant Design 的表格组件能省下非常多的时间。特别是可编辑表格、批量操作、弹窗表单Ant Design 的组件已经把这些交互模式标准化了不太容易写歪。除了组件库状态管理我用的是 Zustand没有用 Redux。LIMS 的全局状态其实不多主要是当前用户信息和登录态页面级数据都放在局部 state 里。Zustand 几十行就能搞定登录态持久化比 Redux 那一套样板代码干净得多。下面这段是package.json里几个关键依赖你可以直接抄{ scripts: { dev: concurrently \npm run server\ \npm run client\, server: nodemon src/server/index.js, client: vite, build: vite build }, dependencies: { express: ^4.19.2, better-sqlite3: ^9.4.3, jsonwebtoken: ^9.0.2, multer: ^1.4.5-lts.1, docx: ^8.2.1 }, devDependencies: { vite: ^5.2.0, react: ^18.2.0, react-dom: ^18.2.0, antd: ^5.15.0, zustand: ^4.5.2 } }这里用better-sqlite3而不是sqlite3是因为它是同步 API写业务逻辑时不用担心回调嵌套配合 Express 路由层天然隔离。docx库用于生成 Word 报告后面会讲怎么用。concurrently用来同时启动前后端开发服务本地联调时一条命令全起。3.3 项目目录结构与环境变量配置一个能让你三个月后还看得懂的 LIMS 项目目录结构要按业务模块分而不是按文件类型分。下面是我常用的一种结构lims/ ├── src/ │ ├── server/ │ │ ├── index.js # Express 入口 │ │ ├── db/ │ │ │ ├── index.js # 数据库连接 │ │ │ └── schema.sql # 建表脚本 │ │ ├── middleware/ │ │ │ └── auth.js # JWT 鉴权中间件 │ │ ├── routes/ │ │ │ ├── auth.js │ │ │ ├── sample.js │ │ │ ├── task.js │ │ │ └── report.js │ │ └── services/ │ │ ├── sampleService.js │ │ ├── taskService.js │ │ └── reportService.js │ └── client/ │ ├── src/ │ │ ├── pages/ │ │ ├── components/ │ │ └── stores/ │ └── vite.config.js ├── .env └── package.json.env文件最容易踩坑。团队里有人会把数据库路径写成绝对路径提交到代码仓库换一台电脑就启动失败。我一般在.env里放DB_PATH./data/lims.db、JWT_SECRETchange-me并把.env加入.gitignore同时提供.env.example作为模板。启动时读不到JWT_SECRET就直接抛错不给你留侥幸空间。4. 把核心功能跑起来从登录鉴权到报告生成的代码实现4.1 基于 JWT 的登录鉴权和按钮级权限控制LIMS 角色一般分成管理员、实验室主管、检测员、复核人、访客五类。JWT 令牌里除了存用户 ID还要存角色代码。前端在发起请求时把令牌放Authorization头里后端中间件校验后把用户信息挂到req.user。// middleware/auth.js const jwt require(jsonwebtoken); module.exports (requiredRoles []) { return (req, res, next) { const token req.headers.authorization?.replace(Bearer , ); if (!token) return res.status(401).json({ error: 未登录 }); try { const decoded jwt.verify(token, process.env.JWT_SECRET); req.user decoded; if (requiredRoles.length !requiredRoles.includes(decoded.role)) { return res.status(403).json({ error: 权限不足 }); } next(); } catch (err) { return res.status(401).json({ error: 登录已过期 }); } }; };中间件返回一个函数调用时传权限数组比如auth([ADMIN, SUPERVISOR])表示只有这两个角色能访问。按钮级权限前端也要做做法是登录后从/api/auth/me拉取当前用户的角色用条件渲染控制按钮显隐后端再做二次校验。前后端双重过滤能挡住 90% 的越权操作。4.2 样品登记与状态流转一个支持事务的接口实现样品登记不只是插入一条 sample 记录还要同时插入多个 sample_item。这一步必须放在数据库事务里否则会出现样品头存在但检测项丢失的情况。// services/sampleService.js const db require(../db); function createSample({ sampleNo, sampleName, typeCode, customer, items }) { const tx db.transaction(() { const info db.prepare( INSERT INTO sample (sample_no, sample_name, sample_type_code, customer_name, status_code) VALUES (?, ?, ?, ?, REGISTERED) ).run(sampleNo, sampleName, typeCode, customer); const sampleId info.lastInsertRowid; const insertItem db.prepare( INSERT INTO sample_item (sample_id, item_code, method_ref, limit_value, unit) VALUES (?, ?, ?, ?, ?) ); for (const item of items) { insertItem.run(sampleId, item.itemCode, item.methodRef, item.limitValue, item.unit); } }); tx(); return { sampleId: db.prepare(SELECT last_insert_rowid() AS id).get().id }; }注意better-sqlite3的db.transaction是同步事务如果回调里抛异常自动回滚。参数items是从前端传来的数组每个元素对应一条检测项。接口层在调用这个服务前必须先用Array.isArray和字段非空校验因为检测项为空时事务也能跑成功但业务上根本不允许。状态流转我写了一个专门的服务函数任何前端都不能直接改status_code必须调用对应的动作函数function changeSampleStatus(sampleId, event) { const stateMachine { REGISTERED: { assign: DETECTING }, DETECTING: { submit: REVIEWING, return: REGISTERED }, REVIEWING: { approve: REPORT_PENDING, reject: DETECTING }, REPORT_PENDING: { release: RELEASED } }; const row db.prepare(SELECT status_code FROM sample WHERE id ?).get(sampleId); const next stateMachine[row.status_code]?.[event]; if (!next) throw new Error(状态不允许从 ${row.status_code} 通过 ${event} 变更); db.prepare(UPDATE sample SET status_code ? WHERE id ?).run(next, sampleId); }这套状态机看起来多写了几行代码但实际救了我很多次。业务人员有时会用系统外的口头约定要求“直接把样品改成已完成”你得在接口层就拦下来否则后续检测项未录入的报告会流向客户。4.3 报告模板动态生成把检测结果填进 Word/PDF报告生成是 LIMS 里最容易让用户质疑系统价值的功能。我采用docx库在服务端生成 Word 报告再调用 LibreOffice 做 PDF 转换。docx的好处是直接用代码构造文档结构不需要手工维护模板文件。const { Document, Packer, Paragraph, Table, TableRow, TableCell } require(docx); async function buildReport(sampleId) { const sample db.prepare(SELECT * FROM sample WHERE id ?).get(sampleId); const items db.prepare(SELECT * FROM sample_item WHERE sample_id ?).getAll(sampleId); const rows items.map(item new TableRow({ children: [ new TableCell({ children: [new Paragraph(item.item_code)] }), new TableCell({ children: [new Paragraph(item.result_value || 未检出)] }), new TableCell({ children: [new Paragraph(item.unit || )] }), new TableCell({ children: [new Paragraph(item.method_ref || )] }) ] })); const doc new Document({ sections: [{ children: [ new Paragraph(样品编号${sample.sample_no}), new Paragraph(样品名称${sample.sample_name}), new Paragraph(检测结果), new Table({ rows: [new TableRow({ children: [ new TableCell({ children: [new Paragraph(项目)] }), new TableCell({ children: [new Paragraph(结果)] }), new TableCell({ children: [new Paragraph(单位)] }), new TableCell({ children: [new Paragraph(方法)] }) ]})].concat(rows) }) ] }] }); const buffer await Packer.toBuffer(doc); return buffer; }这段代码里最需要注意的是中文问题。Node.js 端生成 docx 时字形由打开文档的电脑负责通常不会丢字但如果实验室电脑没有安装中文字体Word 打开会乱码。解决方法是报告生成前检查服务器或客户端是否安装了SimSun或Microsoft YaHei没有就引导用户装字体别省这一步。PDF 转换耗时较长建议放到异步任务里处理前端生成后轮询文件状态。我的经验是单个报告转换大约 2 到 5 秒如果同时导出几十份一定用队列否则服务直接卡死。5. LIMS 上线避坑我在三个项目里踩过的真实问题5.1 现象数据字典改不动已关联数据全乱某实验室上线两个月后发现一个检测项目改名后历史报告里的项目名全被批量改掉了。原因是我当时把检测项名称直接存在sample_item表里而字典表只是表单下拉框的数据源没有建立外键关联。解决方法是把所有历史数据统一按旧名称刷一遍编码然后在sample_item表里只存item_code显示名称全部通过item_code关联字典表实时翻译。从那以后的规则是业务表里永远不存可枚举字段的中文名只存编码。5.2 现象并发分配任务时样品被重复领取检测员同时点“领取任务”按钮后台出现了两个人都拿到同一条任务的异常。原因是我在任务分配接口里写了“先查询未分配任务再更新分配人”没有锁。两个请求同时查到同一条未分配记录各自更新成功。解决方法是给任务表加一个assignee字段并在更新语句里带上条件UPDATE task SET assignee ?, status ASSIGNED WHERE id ? AND assignee IS NULL如果rowCount为 0说明已经被别人领走了直接返回“任务已被领取”。更新语句自带原子性不需要额外锁也不影响性能。5.3 现象报告导出中文乱码或字体错位用户反馈导出的 Word 报告里中文变成方框复制到其他电脑又正常。原因不是代码问题而是报告生成服务器上的字库不完整。有些精简版 Linux 服务器没有安装中文字体LibreOffice 转换 PDF 时只能用空白替代。解决方法是执行apt-get install fonts-noto-cjk安装中文字体并重启转换服务。之后所有报告导出前都做一次字体存在性检查不完整就直接拒绝生成避免交付垃圾文件给客户。5.4 现象浏览器兼容性导致旧设备无法录入实验室前台的旧电脑还在用 IE11 内核React 18 完全不支持页面白屏。原因是开发时没确认现场设备环境只顾着用现代浏览器联调。解决方法是把前端构建目标降到es2015并在index.html里引入babel-polyfill。但说实话 IE 已经是历史包袱我更建议直接给实验室申请两台新电脑跑 Chrome远比在兼容性上磨时间划算。如果领导不批预算至少也得用 Electron 打包一个桌面壳别指望 IE 能安稳跑 Ant Design。6. 让 LIMS 真正接手业务验证清单与数据迁移技巧6.1 上线前跑一遍完整业务模拟脚本我在正式切换前会写一个自动化冒烟脚本按真实业务顺序调接口创建客户 → 登记样品 → 分配任务 → 录入结果 → 复核通过 → 生成报告。每次改完代码都跑一遍确认状态流没有断。curl -X POST /api/sample -d {sampleNo:TEST20240101,items:[...]} curl -X POST /api/task/assign -d {sampleId:1,assignee:detector01} curl -X POST /api/result -d {itemId:1,resultValue:0.03} curl -X POST /api/report/generate -d {sampleId:1}脚本跑完检查最后一份报告的 PDF 文件能不能打开再人工核对两三个样品的数据。这一步能发现 80% 的配置问题。6.2 用脚本把 Excel 历史数据迁移进系统并给检测项留扩展位历史数据迁移最大的坑是 Excel 里的空白行、单位不统一、日期格式混乱。我一般先用一个 Node 脚本读取 Excel清洗后用事务批量插入。插入前把每个检测项都映射到字典表的item_code映射不到的先建字典项再插入数据。这样迁移完新老数据在报表里能统一查询不出现一半有编码一半是中文。我会额外强调一个习惯所有涉及“其他”的选项在字典表里都预留EXT_CUSTOM_1到EXT_CUSTOM_5这几个扩展编码。当实验室某天想加一个“其他说明”字段不需要改表直接用预留编码即可。这个习惯让我免去了好多次凌晨被叫起来加字段的痛苦。从那以后我每次做 LIMS 项目都会强制自己在开发周期里安排一次“真实数据压测”拿实验室过去三个月的真实 Excel 数据手动模拟录入一遍跑完整业务流程。这个过程暴露的问题比任何代码 review 都多。希望帮到你也希望这套 JavaScript 实现的 LIMS 骨架能让你少踩几个我踩过的坑。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。