简介:LIMS(实验室信息管理系统)是一套基于JavaScript开发的Web系统源码,面向实验室信息化建设人员、LIMS实施工程师及需要规范化样品全流程管理的中小企业技术团队,主要用于解决样本跟踪、实验流程自动化、多来源数据整合、报告生成与合规审查等问题。资源压缩包以zip形式提供,大小约52.49MB,下载页未给出文件总数与具体类型明细,建议以完整项目源码对待。系统覆盖样本登记与状态流转、预定义流程自动化、仪器数据集成、自定义报告输出、GLP/GMP合规性支持、角色权限控制、资源调度与库存管理、仪表板实时监控等核心模块;技术架构上采用React/Angular类前端配合Node.js、Java/.NET类后端,并可通过MySQL/Oracle进行数据持久化。已有1740人学习。对于具备JavaScript基础并希望进入LIMS领域的开发者,这是一份可直接开展二次开发与架构研究的参考源码,能够帮助理解实验室业务闭环、前后端分层设计及多表数据交互逻辑。
1. LIMS 是什么:实验室信息管理系统,到底在管什么
实验室里最扎心的场景不是仪器坏了,而是评审老师问「这批样品的原始记录在哪」,你翻了三台电脑和两个柜子也没凑齐。缺的不是勤奋,是能把样品、任务、结果、报告穿成一条线的系统——LIMS,实验室信息管理系统(Laboratory Information Management System)。它做的事听起来不复杂:样品登记、任务分配、结果录入、报告生成、数据追溯。但正因为它听起来不复杂,很多团队用 Excel 顶了三年,最后在合规审查面前集体翻车。
这篇笔记面向检测实验室、研发实验室和质检部门的实施负责人,讲清楚 LIMS 的核心数据模型怎么设计、开源技术栈怎么快速跑通最小流程,以及落地过程中最容易交学费的五个坑。
2. LIMS 设计先行:核心功能与数据模型,画不好这张图后面全是返工
选型之前先画数据流。商业 LIMS 的功能清单动辄上百项,但支撑整个系统的其实是样品、任务、结果、报告这条主链。主链上的数据关系没理清,后面上再多模块都是往沙地上盖楼。
2.1 样品全生命周期:从「登记」到「销毁」的状态机
样品在实验室里不是静态的。从来样登记、领样、上机检测、出结果、审核、出具报告,到最后的留样和销毁,每个环节都对应操作和责任人。很多团队设计时最容易犯的错,是只给样品加一个状态字段,但没有任何流转约束,结果实验员随手把样品从「已登记」改成「已销毁」,中间过程全丢。
我一般会在状态字段之外,再定义一张状态流转表,代码里直接限制哪些状态可以跳到哪些状态。下面是其中一个常用写法,用字典表示允许的迁移路径:
SAMPLE_TRANSITIONS = { "已登记": ["已领样", "作废"], "已领样": ["检测中", "退回"], "检测中": ["已完成", "复测"], "已完成": ["已留样", "已销毁"], "已留样": ["已销毁"], } def transition(sample, to_status, operator): allowed = SAMPLE_TRANSITIONS.get(sample.status, []) if to_status not in allowed: raise ValueError(f"不允许从 {sample.status} 变更为 {to_status}") sample.status = to_status sample.save()这段代码的逻辑不复杂:任何状态变更都必须经过 transition 函数,而不是直接 sample.status = 某个值。好处有两点。第一,界面层和接口层复用同一套校验逻辑,不会出现 Web 端能改、接口端不能改的差异。第二,未来要加「复测」这类分支状态时,只改字典不改业务代码。参数上要注意 allowed 列表的含义是「允许跳转到的目标状态」,不包含当前状态自身,所以重复提交不会造成空操作假象。
这里还有一个设计点值得提:状态流转时要记录操作者和操作时间,这是审计追踪的基础数据。严格一点的实验室还会要求填变更原因,复测、退回这种异常流转必须有备注,否则评审时解释不清。
2.2 任务分配与工作量统计:检测任务怎么流转才不会被卡死
样品进来后,要拆成检测任务派给对应实验员。常见坑是任务只建不跟,某位实验员请假两周,他名下的任务全部滞留,直到要出报告才发现。所以任务表里至少要保存状态、截止时间、指派人三个字段,并用一个待办看板实时汇总。
任务状态一般比样品简单:待接收、检测中、已完成、已退回。有的实验室不设「待接收」,样品直接派到人就算检测中,但这会给后续追溯造成麻烦,因为你不知道任务是什么时候真正被接手的。任务统计的核心诉求是回答两个问题:每个人手上堆了多少活,哪些任务要到期了。下面这条 SQL 是待办看板最常用的查询:
-- 待办看板:按实验员汇总未完成任务数和超期任务数 SELECT a.real_name, COUNT(t.id) AS pending_tasks, SUM(CASE WHEN t.due_at < NOW() AND t.status != '已完成' THEN 1 ELSE 0 END) AS overdue_tasks FROM task t JOIN analyst a ON t.assignee_id = a.id WHERE t.status IN ('待接收', '检测中') GROUP BY a.id ORDER BY overdue_tasks DESC, pending_tasks DESC;这条查询把待办量和超期量同时算出来,看板上按超期数量排序,管理者一眼就能看到哪些人有风险。参数上要注意,超期判断必须排除已完成任务,否则已完成但超期交单的任务会被重复计数。统计维度按人分组即可,不需要再按样品分组,因为任务才是独立的指派单元。
任务分配策略在不同实验室差别很大。小团队一般手动指派,靠管理员对每个实验员能力范围的熟悉程度。大团队建议在任务创建时做自动分配,规则通常是「按项目隔离 + 按工作量均衡 + 按方法资质过滤」。自动分配不是越高大上越好,见过不少项目在自动分配规则上花了几个月,最后回归手动。建议先手动跑通,再逐步把规则沉淀成代码。
2.3 结果录入与报告生成:从原始记录到可追溯报告的数据链路
结果数据是 LIMS 的核心资产,但很多系统把它设计得太简单:在任务表里塞几个结果字段就完事。实际上一份样品可能测多个项目,一个项目可能用不同方法测多次,还可能有一次复测,所以结果必须独立成表,和任务是多对一的关系。
结果数据的来源通常有三类:人工录入、仪器文件导入、中间数据库同步。无论哪种来源,结果记录上至少要带四个属性:对应的任务、检测项目、数值和单位、录入人。审核是另一个关键环节。只设「待审核 / 已审核」两种状态的系统,审核发现问题时没有退回出口,实验员只能删除重录,审计日志里留下删除记录,反而更难解释。建议至少加一个「退回」状态,审核不过的结果退给实验员修改,完整保留修改历史。
报告生成要解决的问题是模板绑定和签字。常见做法是把报告模板做成 Word 或 HTML 模板,用占位符绑定样品信息、结果列表和结论字段,生成时用模板引擎渲染。签字建议定成两级审核:实验员对结果负责,审核人对报告签发负责,系统里必须能查到每一步是谁、什么时间、做了什么操作。
2.4 核心表结构:样品、任务、结果三张表怎么定义字段
把上面三节的设计落到表结构上,就是三张核心表加若干配置表。设计原则是:样品表管来样信息,任务表管执行过程,结果表管检测数据,各管一段,不互相塞字段。下面是我常用的字段清单,供建模时对照。
样品表的关键字段:
| 字段名 | 类型 | 说明 | 备注 |
|---|---|---|---|
| id | bigint | 主键 | 自增 |
| code | varchar(32) | 样品编号 | 唯一,如 S2024-0001 |
| name | varchar(128) | 样品名称 | 用户可读 |
| status | varchar(16) | 当前状态 | 见状态机,默认已登记 |
| received_at | datetime | 收样时间 | 用于时效统计 |
| storage_info | varchar(255) | 留样位置 | 可空,用于追溯 |
| remark | text | 备注 | 保留给异常说明 |
任务表的关键字段:
| 字段名 | 类型 | 说明 | 备注 |
|---|---|---|---|
| id | bigint | 主键 | |
| sample_id | bigint | 关联样品 | 外键,禁止级联删除 |
| assignee_id | bigint | 指派人 | 关联人员表 |
| status | varchar(16) | 任务状态 | 待接收/检测中/已完成/已退回 |
| due_at | datetime | 截止时间 | 用于超期告警 |
| priority | smallint | 优先级 | 0 普通,1 加急 |
结果表的关键字段:
| 字段名 | 类型 | 说明 | 备注 |
|---|---|---|---|
| id | bigint | 主键 | |
| task_id | bigint | 关联任务 | 外键 |
| item_name | varchar(64) | 检测项目 | 如含量、水分 |
| value | numeric | 检测数值 | 部分项目为空 |
| unit | varchar(16) | 单位 | 如 %、mg/kg |
| status | varchar(16) | 审核状态 | 待审核/已审核/已退回 |
| created_by | bigint | 录入人 | 关联用户表 |
| created_at | datetime | 录入时间 | 审计用 |
提示:三个 id 建议用 bigint 自增。除非你有明确的跨库合并需求,否则不要为了「将来可能要分布式」提前用雪花 ID,徒增复杂度。
这里有两个容易被忽略的细节。第一,样品表和任务表之间不要用级联删除,样品登记过的记录即使作废也要保留,这是合规要求的一条底线。第二,状态字段用字符串而不是布尔值,因为状态不是两个而是一组枚举,后续加状态不用改表结构。数值字段用 numeric 而不是 float,检测结果经常要精确到小数点后四位,浮点误差在这种场景下会引发不必要的争议。
3. 用开源技术栈搭一个最小可用 LIMS:从建表到跑通首单检测
设计完成后,怎么快速验证这套模型能不能跑?常见做法是选开源技术栈自己搭一个最小系统。不必一开始就买商业产品,采购周期长,需求冻结后改造成本高。先自研跑通,再决定是否迁到成熟方案,是更稳的路径。
3.1 技术选型:为什么我推荐 Python/Django 加 PostgreSQL
先说结论,中小实验室的 LIMS 自研或二开,我一般推荐 Python 生态。理由有三。第一,Django 自带 Admin、迁移和权限体系,CRUD 类的管理后台几乎不用写。第二,Python 在数据处理上的生态成熟,仪器文件解析、结果计算、报告生成都有现成库。第三,开源 LIMS 里 Python 技术栈的占比不低,将来想从自研切到开源方案,迁移成本相对低。
Java/Spring 也是成熟选项,尤其是在大型制造企业的质量系统里,集成能力更强。.NET 在仪器生态里有历史优势,不少进口仪器的 SDK 提供 .NET 接口。但这两条路对团队技能要求更高。如果实验室 IT 团队只有两三个人,Python 的迭代速度明显占优。数据库选 PostgreSQL 而不是 MySQL,主要原因是检测数据里字符集和 JSON 扩展需求多,PG 的 JSONB 字段和检查约束在结果灵活性上更好用。
需要说明的是,这不是让你从零造轮子。现在有不少开源的 LIMS 方案,但它们的定制成本经常被低估。我见到的做法是:先用轻量级自研跑通最小流程,验证数据模型合理,再决定继续自研还是迁移。下面所有命令都按这个思路来。
3.2 Docker Compose 拉起基础环境:最小可复现的一步
环境准备直接用 Docker Compose,好处是团队里任何人 clone 下来一条命令就能起环境,不用在每台机器上手动装数据库。下面是最小化的编排文件:
version: "3.8" services: db: image: postgres:15 environment: POSTGRES_DB: lims POSTGRES_USER: lims POSTGRES_PASSWORD: lims_pass volumes: - pgdata:/var/lib/postgresql/data # 数据卷,容器删除后数据不丢 ports: - "5432:5432" web: build: . command: python manage.py runserver 0.0.0.0:8000 volumes: - .:/app ports: - "8000:8000" depends_on: - db environment: DATABASE_URL: postgres://lims:lims_pass@db:5432/lims volumes: pgdata:启动时执行docker compose up -d,接着执行docker compose exec web python manage.py migrate初始化数据库结构。这套编排里值得注意的参数是 db 服务的 volumes,它将 PostgreSQL 的数据目录挂到命名卷 pgdata 上,容器删了重建数据还在,不会因为一次docker compose down把样品数据全清掉。web 服务挂载当前目录到 /app,开发阶段改代码不用重新构建镜像。
注意:depends_on 只保证容器启动顺序,不保证数据库已可接受连接。应用启动时要加等待逻辑,比如在入口脚本里循环检查数据库端口,否则第一次启动大概率连不上数据库,这是新手最容易遇到的玄学问题。
3.3 用 ORM 定义核心模型:三张表的代码实现
数据库初始化之后,核心是把第二章的表结构写成 ORM 模型。下面是 Django 里的一个简化实现,和前面的字段清单一一对应:
from django.db import models class Sample(models.Model): code = models.CharField(max_length=32, unique=True) # 样品编号,全局唯一 name = models.CharField(max_length=128) status = models.CharField(max_length=16, default="已登记") received_at = models.DateTimeField() storage_info = models.CharField(max_length=255, blank=True) remark = models.TextField(blank=True) class Task(models.Model): sample = models.ForeignKey(Sample, on_delete=models.PROTECT, related_name="tasks") assignee = models.ForeignKey("auth.User", on_delete=models.PROTECT) status = models.CharField(max_length=16, default="待接收") due_at = models.DateTimeField() priority = models.SmallIntegerField(default=0) class Result(models.Model): task = models.ForeignKey(Task, on_delete=models.PROTECT, related_name="results") item_name = models.CharField(max_length=64) value = models.DecimalField(max_digits=12, decimal_places=4, null=True) unit = models.CharField(max_length=16) status = models.CharField(max_length=16, default="待审核") created_by = models.ForeignKey("auth.User", on_delete=models.PROTECT) created_at = models.DateTimeField(auto_now_add=True)代码里三个要点。第一,外键统一用 on_delete=models.PROTECT,样品一旦被任务引用就不能硬删,只能改状态为作废,这正是第二章强调的合规底线。第二,value 用 DecimalField 而不是 FloatField,max_digits 和 decimal_places 分别控制总位数和小数位,检测结果按四位小数存储,输出时不会出现浮点误差。第三,status 全部用 CharField,建议再加 choices 选项,让 Django 表单自带校验,避免魔法字符串散落各处。
这里有个可以偷懒的地方:Django Admin 会自动生成三张表的增删改查界面,最小流程验证阶段完全够用。不要在前两周就投入写前端页面,先把业务逻辑跑通,界面后面再加。
3.4 跑通最小流程:登记样品、分配任务、录入结果
模型建好后,用 Django shell 跑一遍完整流程,验证数据链路是否通顺:
from lims.models import Sample, Task, Result # 1. 登记样品 s = Sample.objects.create( code="S2024-0001", name="来料批次-001", received_at="2024-06-01 09:30:00", ) # 2. 分配任务 t = Task.objects.create( sample=s, assignee_id=1, status="待接收", due_at="2024-06-03 18:00:00", ) # 3. 接收并检测 t.status = "检测中" t.save() # 4. 录入结果并审核 r = Result.objects.create( task=t, item_name="含量", value="98.2000", unit="%", created_by_id=1, ) r.status = "已审核" r.save() print(s.code, t.status, r.value, r.unit)这段脚本模拟了一次完整的首单检测。注意每个对象创建后,状态变更都通过读取对象、改字段、save 三步完成,没有直接跳过状态。流程跑通后,顺手做一件事:把第二章 SAMPLE_TRANSITIONS 的校验函数接到 Task 的状态变更上,看是否能在非法流转时抛错。这一步虽然简单,但能把「状态机只在文档里、不在代码里」的问题提前暴露。
4. LIMS 落地避坑指南:五个让实施翻车的真实场景
功能可以慢慢加,坑最好提前知道。下面五条来自多个实施项目的复盘,现象、原因、解决都写清楚,你大概率会遇到至少三条。
4.1 坑一:需求调研只问正常流程,异常流程全部漏掉
现象:系统上线后,实验员遇到退样、复测、加测这些操作时完全不走系统,退回用 Excel 记录,等到出报告时发现数据不齐,又回头补录,系统里的数据和实际情况对不上。
原因:调研阶段只按标准作业流程提问,访谈对象都是照章办事的流程负责人,没人主动提异常场景。而实验室日常里退样、复测、结果超限复核的发生频率远高于预期。
解决:调研清单里必须包含异常场景专项,至少覆盖样品退回、复测、加测、结果超限复核、作废后恢复这几类。每类异常都要问三个问题:现在怎么处理、谁有权处理、处理过程要不要留痕。把这些答案转成状态机和权限规则,再开始建表。
4.2 坑二:仪器数据对接做成了点对点文件解析,脚本变成黑匣子
现象:一台进口仪器的导出文件格式调整后,解析脚本直接崩溃,而且没人知道脚本是谁写的、改过几次,最后只能重新逆向文件格式。
原因:对接时图快,写了一个一次性的解析脚本放在服务器目录里,没有版本管理,没有输入校验,报错信息只有「解析失败」四个字。
解决:解析脚本纳入代码库统一管理,文件格式的每个字段写清楚含义和示例。解析入口加格式校验,字段缺失、类型不符时记录到导入日志而不是静默跳过。关键解析脚本配一个样例文件和对应的期望输出,格式一变,跑一遍测试就知道哪里坏了。
4.3 坑三:权限模型「人人管理员」,审计追踪形同虚设
现象:内审发现普通实验员能修改已审核的结果数值,系统里改完连记录都没有,签字环节变成走过场。
原因:系统初始化时为了省事,把权限模板直接选成管理员,所有账号权限一样大。后续虽然换了新模板,但已有账号的权限没有重新刷。
解决:权限模型按角色定义,至少分实验员、审核人、质量管理员、系统管理员四类。结果审核后默认锁定,修改必须走「退回」流程并记录原因。上线前用测试账号按角色清单逐项过一遍权限矩阵,确认越权操作在界面上根本不可见。
4.4 坑四:备份只备数据库,忘了方法文件和仪器配置文件
现象:服务器磁盘故障后,数据库恢复成功,但所有检测方法、报告模板、仪器解析规则全丢了,因为那些文件在应用服务器的本地目录里,压根不在备份范围。
原因:备份方案只考虑了数据库数据目录,忽略了 LIMS 系统里大量文件型资产的存储位置,也没有把文件备份纳入恢复演练。
解决:备份范围分三层:数据库全量备份、应用配置和上传文件目录的定期同步、备份的离线副本。恢复演练至少每季度做一次,重点验证的不是「备份能还原」,而是「还原后系统能正常出报告」。
4.5 坑五:自定义字段随意加,半年后表结构烂尾
现象:每个科室都要求在样品表上加自己的字段,半年后样品表接近两百列,查询变慢,表单页面长得吓人,谁也不敢动这张表。
原因:没有字段治理机制,需求方提一个加一个,方案评审形同虚设。核心表结构被业务侧的需求牵着走。
解决:核心表只保留所有科室通用的字段,科室专属字段放到扩展属性表,或用 JSONB 字段承载。加字段前必须经过数据模型评审,问三个问题:这个字段有多少科室用、存了之后谁来维护、没有它行不行。回答不清楚的,先不进表结构。
5. LIMS 仪器数据对接:输出文件解析、异步导入与结果校验
仪器对接是 LIMS 实施里最容易被低估的环节。采购前销售都说有接口,落地时才发现接口文档三页纸,真正靠得住的反而是文件解析。这一章讲清楚文件解析、导入性能和幂等三个问题。
5.1 仪器输出文件的三种常见格式与解析策略
按可靠性排序,仪器数据对接的优先级是这样:有 SDK/API 用 SDK/API,没有就争取仪器软件导出 CSV 或 XML,最后才解析 PDF。PDF 解析之所以是下策,是因为 PDF 的视觉布局和实际内容顺序经常不一致,检测项目可能被分页拆开,解析规则写起来像玄学,而且仪器厂家更新一个版本就可能让之前的规则全部失效。
CSV 是性价比最高的格式,但有一个隐蔽的坑:用 Excel 另存的 CSV 可能是 GBK 或带 BOM 的 UTF-8,直接用默认编码读取会乱码。下面这段解析函数的开头就处理了编码问题:
import csv from pathlib import Path def parse_instrument_file(path: Path): with path.open("r", encoding="utf-8-sig", newline="") as f: reader = csv.DictReader(f) for row_no, row in enumerate(reader, start=2): sample_code = row.get("样品编号", "").strip() if not sample_code: continue # 空行直接跳过 try: value = float(row.get("结果", "")) except ValueError: raise ValueError(f"第 {row_no} 行结果列无法转换为数值") yield { "row_no": row_no, "sample_code": sample_code, "item_name": row.get("检测项目", "").strip(), "value": value, "unit": row.get("单位", "").strip(), }几个参数值得解释。encoding 用 utf-8-sig,它同时兼容带 BOM 和不带 BOM 的 UTF-8 文件,这是 Excel 导出和仪器软件导出的最常见编码。newline="" 是 csv 模块的官方推荐写法,避免 Windows 行尾符干扰解析。row_no 从 2 开始,因为第 1 行是表头。yield 让这个函数成为生成器,大批量文件解析时不会一次性把整个文件读进内存,几千行的结果文件也能稳定处理。
5.2 大批量导入卡顿:用队列把同步导入改成异步
很多实验室一次批量导入几百个结果很正常。如果在 Web 请求里同步解析和写库,浏览器会一直转圈,请求一超时用户就再点一次,结果导入两遍,这就是「重复导入」问题的起点。常见做法是把导入任务丢进队列,页面立刻返回「导入中」,后台消费者逐行处理并写导入日志。
下面是一个消费者侧的最小实现,生产侧只需要把文件路径塞进队列:
def process_import(file_path: str): total, success, failed = 0, 0, 0 for record in parse_instrument_file(Path(file_path)): total += 1 try: save_result(record) success += 1 except (SampleNotFoundError, ResultDuplicateError) as e: failed += 1 log_import_error(file_path, record["row_no"], str(e)) notify_import_finish(file_path, total, success, failed)这里把 save_result 单独抽出来是有原因的:仪器的每一行结果对应一个样品编号,样品编号在系统里查不到时,整批导入不应该失败,而应该跳过这一行并记录原因,最后汇总一个导入报告给实验员。参数上要注意异常捕获必须细分,SampleNotFoundError 和 ResultDuplicateError 属于可跳过错误,数据格式错误属于文件级错误,两者处理方式不同,前者记录后继续,后者应该中止整个导入。
5.3 幂等校验与导入日志:重复导入不再需要后悔药
重复导入的根治办法是幂等。常见做法是在结果表上加一个来源唯一键 source_key,值由「仪器文件标识 + 行号」拼成。同一文件第二次导入时 source_key 已存在,直接跳过。建表语句如下:
ALTER TABLE result ADD COLUMN source_key varchar(128); CREATE UNIQUE INDEX idx_result_source_key ON result(source_key) WHERE source_key IS NOT NULL;这里用了部分索引(WHERE source_key IS NOT NULL),目的是避免唯一约束把 source_key 为空的记录也纳入限制。人工录入的结果没有 source_key,不受这个索引影响。引入 source_key 后,重复导入问题基本绝迹,用户也不用再担心误操作需要后悔药。
导入日志表是排错的第一手线索,建议保留至少六个月。字段不需要多:原始文件名、出错行号、错误原因、导入时间。仪器厂家改一次导出格式,你从日志里一眼就能看出是哪一天、哪个文件、哪一行的格式开始对不上。
6. 进阶验证:用一条追溯链检验你的 LIMS 是否合格
LIMS 上线三个月后,真正该做的不是加功能,而是验证追溯链。方法是:随机抽一份已签发的报告,从报告号出发反向走完全程——报告由哪些已审核结果组成,这些结果对应哪次检测任务,任务的原始记录文件在哪台仪器上生成,样品的接收登记时间和留样位置是否一致。任何一环查不到,就是系统的薄弱点。
这一步可以定期用 SQL 核查,比如找「有报告但结果未审核」的数据漏洞:
SELECT r.report_no, s.code FROM report r JOIN sample s ON r.sample_id = s.id LEFT JOIN result res ON res.task_id = r.task_id AND res.status != '已审核' WHERE res.id IS NOT NULL;没有返回行,说明报告和已审核结果完全对齐;有返回行,说明数据链路有缺口。建议每月抽五份报告做一次人工追溯演练,把追溯耗时和断点记下来,连续三个月就能看出系统数据质量的改善趋势。这时候的 LIMS 已经不是记录工具,而是实验室的数据中台:样品数据、仪器数据、质量控制数据汇在一起,再往上接统计分析看板,异常趋势、复测率、设备利用率都能在一个界面里看。
我现在养成的习惯是:任何新模块上线前,先写一条追溯查询,确认新模块的数据能被从报告反查到原始记录,查不到就不上线。这个习惯帮我避开了不少返工。希望帮到你。
本文还有配套的精品资源,点击获取