企业做 EHS 不只是贴标签、写报告,最务实的一块就是“健康管理”。员工健康直接影响生产效率、事故率、合规风险和用工成本。把健康管理做成系统化、数据化、可落地的模块,能把体检、档案、劳保用品管理、异常预警、统计看板连成闭环——这对中大型企业尤其有价值。下面这篇文章会把“怎么搭建”讲得尽量接地气、有干货,并给出架构图、流程图、数据库设计、后端 API、前端示例和落地实现建议,代码做成一个相对完整的参考实现,方便直接拿去改造。
注:本文示例所用方案模板:简道云EHS健康安全环境管理系统,给大家示例的是一些通用的功能和模块,都是支持自定义修改的,你可以根据自己的需求修改里面的功能。
本文你将了解
简单说,健康管理板块是 EHS 系统里专门负责“员工健康生命周期管理”的子系统。它把员工从入职、体检、健康档案、职业病监测、到劳保用品的发放与归档、再到健康看板、异常告警、合规报表都纳进来,形成闭环。目标是:让健康数据可查、可追溯、可提醒、可统计,从而降低职业健康风险、减少工伤、提升合规效率与管理可视化。
企业价值点(可量化示例):
下面给出一个常见、可扩展的架构示意,采用前后端分离 + REST API + 定时任务 + 报表服务。
mermaid
graph TD
User[用户(EHS 管理员 / 现场管理员 / 员工)] -->|Web/Mobile| Frontend[React / Mobile App]
Frontend --> API[API 网关 / 后端服务(Node.js / Spring Boot)]
API --> DB[(主库 PostgreSQL)]
API --> FileStore[(对象存储:S3 / MinIO)]
API --> Auth[(认证/授权:OAuth2 / JWT)]
API --> JobScheduler[(定时任务:Cron / BullMQ)]
API --> ES[(ElasticSearch,用于日志/模糊检索)]
API --> BI[(报表/看板服务,Grafana 或 内置)]
subgraph Integrations
API --> HR[HR 系统]
API --> MES[生产系统/门禁/工牌]
API --> Lab[体检中心接口]
end技术选型建议(可替换)
必备模块:
推荐模块(加强版):
下面以“体检入库并预警”、“PPE 入库与领用”两个主流程为例说明。
mermaid
flowchart TD
A[HR/员工触发体检] --> B[体检机构上传体检报告]
B --> C[自动导入 / 手工上传 PDF]
C --> D[解析关键指标(如血压、肺功能等)]
D --> E{是否异常?}
E -- 是 --> F[标注异常、生成异常工单、发送通知给 EHS/HR/员工]
E -- 否 --> G[更新员工健康档案]
F --> H[跟踪复查/记录]
要点:
mermaid flowchart TD A[仓库录入劳保用品入库单] --> B[系统生成库存记录] B --> C[员工/主管发起领用申请] C --> D[仓库/主管审批] D --> E[出库并记录领用单,关单] E --> F[更新看板(库存、领用统计)]

要点:
下面给出核心表的 SQL 建表参考(以 PostgreSQL 为例)。表字段会兼顾审计和扩展性。
sql
-- 员工基本信息
CREATE TABLE employees (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
employee_no VARCHAR(64) UNIQUE NOT NULL,
name VARCHAR(128) NOT NULL,
gender VARCHAR(16),
birthday DATE,
department VARCHAR(128),
position VARCHAR(128),
hire_date DATE,
mobile VARCHAR(32),
email VARCHAR(128),
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
);
-- 员工健康档案(结构化+JSON)
CREATE TABLE health_profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
employee_id UUID REFERENCES employees(id),
medical_history TEXT, -- 既往史,结构化文本或 JSON
allergies TEXT,
occupational_exposure TEXT,
attachments JSONB, -- 存 PDF/图像的存储路径与元数据
last_updated TIMESTAMP DEFAULT now()
);
-- 体检结果(每次体检一条)
CREATE TABLE exam_results (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
employee_id UUID REFERENCES employees(id),
exam_date DATE,
exam_type VARCHAR(64), -- 入职/年度/离职/专项
indicators JSONB, -- {"blood_pressure":"120/80","bmi":23.5, ...}
summary TEXT,
report_path TEXT, -- 对象存储地址
abnormal_flags JSONB, -- {"blood_pressure":true}
created_at TIMESTAMP DEFAULT now()
);
-- 劳保用品资料库
CREATE TABLE ppe_catalog (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sku VARCHAR(64) UNIQUE NOT NULL,
name VARCHAR(128),
spec VARCHAR(128),
unit VARCHAR(32),
safety_standards TEXT,
created_at TIMESTAMP DEFAULT now()
);
-- 劳保库存(实时)
CREATE TABLE ppe_stock (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ppe_id UUID REFERENCES ppe_catalog(id),
batch_no VARCHAR(64),
qty INT,
location VARCHAR(128),
in_date DATE,
expire_date DATE,
created_at TIMESTAMP DEFAULT now()
);
-- 劳保领用单
CREATE TABLE ppe_issuances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
issuance_no VARCHAR(64) UNIQUE,
employee_id UUID REFERENCES employees(id),
ppe_id UUID REFERENCES ppe_catalog(id),
qty INT,
issued_by VARCHAR(128),
issued_at TIMESTAMP DEFAULT now(),
purpose TEXT,
approval_status VARCHAR(32) DEFAULT 'pending', -- pending/approved/rejected
approval_info JSONB
);
-- 异常工单
CREATE TABLE health_alerts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
employee_id UUID REFERENCES employees(id),
source VARCHAR(64), -- exam/manager/auto
alert_type VARCHAR(64),
content TEXT,
status VARCHAR(32) DEFAULT 'open', -- open/closed
created_at TIMESTAMP DEFAULT now(),
resolved_at TIMESTAMP
);这些表可以满足常见场景,JSONB 用来保持灵活性(例如指标变化、不同医院返回结构不同),后续可逐步结构化。
下面给出一个整合的示例代码,包含模型、部分路由与关键业务逻辑(导入体检、PPE 领用、看板统计)。这是一个精简但可运行的参考实现,用作项目启动模板。
环境:Node.js 18+,Postgres,Sequelize ORM,Express
js
// app.js (入口)
const express = require('express');
const bodyParser = require('body-parser');
const { sequelize } = require('./models');
const routes = require('./routes');
const app = express();
app.use(bodyParser.json());
app.use('/api', routes);
const PORT = process.env.PORT || 3000;
sequelize.authenticate().then(()=> {
console.log('DB connected');
app.listen(PORT, ()=> console.log(`Server running ${PORT}`));
}).catch(err => console.error(err));
js
// models/index.js
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize(process.env.DATABASE_URL || 'postgres://user:pass@localhost:5432/ehs', {
logging:false
});
const Employee = sequelize.define('employee', {
id: { type: DataTypes.UUID, primaryKey:true, defaultValue: Sequelize.literal('gen_random_uuid()') },
employee_no: { type: DataTypes.STRING, unique:true },
name: DataTypes.STRING,
department: DataTypes.STRING,
position: DataTypes.STRING,
});
const HealthProfile = sequelize.define('health_profile', {
id: { type: DataTypes.UUID, primaryKey:true, defaultValue: Sequelize.literal('gen_random_uuid()') },
medical_history: DataTypes.TEXT,
allergies: DataTypes.TEXT,
attachments: DataTypes.JSONB
});
const ExamResult = sequelize.define('exam_result', {
id: { type: DataTypes.UUID, primaryKey:true, defaultValue: Sequelize.literal('gen_random_uuid()') },
exam_date: DataTypes.DATE,
exam_type: DataTypes.STRING,
indicators: DataTypes.JSONB,
report_path: DataTypes.STRING,
abnormal_flags: DataTypes.JSONB
});
const PpeCatalog = sequelize.define('ppe_catalog', {
id: { type: DataTypes.UUID, primaryKey:true, defaultValue: Sequelize.literal('gen_random_uuid()') },
sku: { type: DataTypes.STRING, unique:true },
name: DataTypes.STRING,
spec: DataTypes.STRING,
unit: DataTypes.STRING
});
const PpeIssuance = sequelize.define('ppe_issuance', {
id: { type: DataTypes.UUID, primaryKey:true, defaultValue: Sequelize.literal('gen_random_uuid()') },
issuance_no: DataTypes.STRING,
qty: DataTypes.INTEGER,
purpose: DataTypes.TEXT,
approval_status: { type: DataTypes.STRING, defaultValue:'pending' }
});
const HealthAlert = sequelize.define('health_alert', {
id: { type: DataTypes.UUID, primaryKey:true, defaultValue: Sequelize.literal('gen_random_uuid()') },
source: DataTypes.STRING,
alert_type: DataTypes.STRING,
content: DataTypes.TEXT,
status: { type: DataTypes.STRING, defaultValue:'open' }
});
// 关系
Employee.hasOne(HealthProfile, { foreignKey:'employee_id' });
Employee.hasMany(ExamResult, { foreignKey:'employee_id' });
Employee.hasMany(PpeIssuance, { foreignKey:'employee_id' });
Employee.hasMany(HealthAlert, { foreignKey:'employee_id' });
PpeCatalog.hasMany(PpeIssuance, { foreignKey:'ppe_id' });
module.exports = { sequelize, Employee, HealthProfile, ExamResult, PpeCatalog, PpeIssuance, HealthAlert };
js
// routes/index.js
const express = require('express');
const router = express.Router();
const { Employee, ExamResult, PpeCatalog, PpeIssuance, HealthAlert, sequelize } = require('../models');
// 创建员工(示例)
router.post('/employees', async (req, res) => {
const emp = await Employee.create(req.body);
res.json(emp);
});
// 导入体检结果(简化)
router.post('/exam/import', async (req, res) => {
// body: { employee_no, exam_date, exam_type, indicators, report_path }
const t = await sequelize.transaction();
try {
const { employee_no, exam_date, exam_type, indicators, report_path } = req.body;
const emp = await Employee.findOne({ where:{ employee_no }});
if(!emp) return res.status(404).json({error:'员工不存在'});
// 简单异常判断规则示例(血压)
const abnormal_flags = {};
if(indicators && indicators.blood_pressure){
const [sys,dia] = indicators.blood_pressure.split('/').map(Number);
if(sys>140 || dia>90) abnormal_flags.blood_pressure = true;
}
const exam = await ExamResult.create({ employee_id: emp.id, exam_date, exam_type, indicators, report_path, abnormal_flags }, { transaction: t });
if(Object.keys(abnormal_flags).length>0){
await HealthAlert.create({ employee_id: emp.id, source:'exam', alert_type:'exam_abnormal', content:`检测到异常指标: ${JSON.stringify(abnormal_flags)}` }, { transaction:t });
// 这里可触发通知:邮件/短信/站内消息
}
await t.commit();
res.json({ ok:true, exam });
} catch(err){
await t.rollback();
console.error(err);
res.status(500).json({ error: err.message });
}
});
// PPE 领用申请(创建)
router.post('/ppe/apply', async (req, res) => {
// body: { employee_no, sku, qty, purpose }
const { employee_no, sku, qty, purpose } = req.body;
const emp = await Employee.findOne({ where:{ employee_no }});
if(!emp) return res.status(404).json({error:'员工不存在'});
const ppe = await PpeCatalog.findOne({ where:{ sku }});
if(!ppe) return res.status(404).json({error:'物品不存在'});
const iss = await PpeIssuance.create({ issuance_no: `ISS-${Date.now()}`, employee_id: emp.id, ppe_id: ppe.id, qty, purpose, approval_status:'pending' });
// 可触发审批流
res.json(iss);
});
// 统计接口(看板)示例:体检异常人数、近 30 天 PPE 领用量
router.get('/dashboard/summary', async (req, res) => {
const totalEmployees = await Employee.count();
const abnormalCount = await HealthAlert.count({ where:{ alert_type:'exam_abnormal', status:'open' }});
const recentPpe = await PpeIssuance.findAll({
attributes:[
'ppe_id',
[sequelize.fn('sum', sequelize.col('qty')), 'total_qty']
],
group:['ppe_id'],
limit:10
});
res.json({ totalEmployees, abnormalCount, recentPpe });
});
module.exports = router;说明与落地要点
下面给出两个关键页面的示例:体检导入/查看、健康看板。代码为精简版,实际要结合项目脚手架来集成。
jsx
// components/ExamUpload.jsx
import React, {useState} from 'react';
import { Upload, Button, Input, DatePicker, message } from 'antd';
import axios from 'axios';
export default function ExamUpload() {
const [employeeNo, setEmployeeNo] = useState('');
const [examDate, setExamDate] = useState(null);
const [uploading, setUploading] = useState(false);
const [file, setFile] = useState(null);
const handleUpload = async () => {
if(!employeeNo || !examDate || !file) return message.warn('请补充信息');
setUploading(true);
try{ // 示例:先上传文件到后端(或直传 S3),然后提交结构化指标(这里简化)
const fd = new FormData();
fd.append('file', file);
const r1 = await axios.post('/api/files/upload', fd);
await axios.post('/api/exam/import', {
employee_no: employeeNo, exam_date: examDate.format('YYYY-MM-DD'),
exam_type: '年度', indicators: {}, report_path: r1.data.path
});
message.success('导入成功');
}catch(err){ message.error('导入失败'); console.error(err); }
setUploading(false);
};
return (
<div>
<Input placeholder="员工编号" value={employeeNo} onChange={e=>setEmployeeNo(e.target.value)} />
<DatePicker onChange={d=>setExamDate(d)} />
<Upload beforeUpload={f=>{ setFile(f); return false; }}>
<Button>上传体检单 PDF</Button>
</Upload>
<Button type="primary" onClick={handleUpload} loading={uploading}>导入体检</Button>
</div>
);
}
jsx
// components/Dashboard.jsx
import React, {useEffect, useState} from 'react';
import axios from 'axios';
import { Card, Statistic, Table } from 'antd';
import { Line } from 'recharts';
export default function Dashboard() {
const [data, setData] = useState({});
useEffect(()=>{ axios.get('/api/dashboard/summary').then(r=>setData(r.data)); },[]);
return (
<div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:16}}>
<Card><Statistic title="员工总数" value={data.totalEmployees} /></Card>
<Card><Statistic title="未处理体检异常" value={data.abnormalCount} /></Card>
<Card style={{gridColumn:'1 / span 2'}}>
<h3>最近 PPE 领用(Top10)</h3>
<Table dataSource={data.recentPpe || []} rowKey="ppe_id">
<Table.Column title="PPE ID" dataIndex="ppe_id" key="ppe_id" />
<Table.Column title="领用总量" dataIndex="total_qty" key="total_qty" />
</Table>
</Card>
</div>
);
}前端要点
在这里我给大家推荐一个业务人员就能够直接上手的高性价比、零代码平台——简道云EHS 健康安全环境管理系统,简道云背靠国内BI龙头帆软,在数据处理、数据展示上的能力有绝对优势,数据分析支持高度自定义,任何分析需求都可以快速制作仪表盘,简道云EHS 健康安全环境管理系统涵盖了核心 8 大业务模块,高效全面地满足安全管理核心需求

落地后建议检验的几个交付点:
验收 KPI(建议):
FAQ 1:体检结果来自不同医院、格式差异大,如何可靠地导入并判断异常?
很多企业遇到的痛点就是体检机构给的数据格式不统一:有的直接给 PDF,有的给 Excel 或结构化接口。实战建议是走两条路:一是优先接入体检机构的结构化 API(或约定 CSV 模板),实现自动化导入;二是对于仍然是 PDF 的机构,先做人工+半自动化(OCR 提取关键指标)并由 EHS/医务人员审核后入库。异常判定不要把阈值写死在代码里,应做成可配置规则(rule table),可以按性别、年龄段、岗位不同阈值,同时支持人工复核机制。最稳妥的方式是“自动判定 + 人工确认”,这样既高效又减少误报、漏报风险。
FAQ 2:员工健康数据很敏感,怎么保证隐私又能方便管理?
健康数据属于高度敏感信息,因此系统设计时必须把“最小权限原则”放第一位:只有经过授权的角色(如 EHS 管理员、授权医务人员)可以查看完整报告,普通主管只能看到是否合格/是否有异常的结论性字段。对外导出时默认进行脱敏(姓名可替换为工号/或部分掩码),并记录所有访问日志。传输和存储均使用 TLS + 对象存储加密(SSE)。同时建议建立数据保留策略(例如体检报告保存期限、敏感字段加密保存等),并在员工入职手册中说明数据使用范围、获得员工同意,满足法律合规要求。
FAQ 3:PPE(劳保用品)经常乱领、库存不准,系统如何落地减少浪费?
落地上要把“场景”想清楚。首先,把领用流程线上化,所有领用要有申请、审批、出库三步并记录用途与岗位。其次,启用“扫码领用”或“发放台账”,领用时绑定员工工号和目的,仓库发放时核验并签字确认,现场主管审批异常用量。系统引入周期性库存盘点(盘点单模块),并与采购结合,当库存低于安全库存自动触发采购预警。最后,可以把领用和考勤/岗位绑定:对于高耗材岗位按班次或周期自动分配,减少随意领用。通过流程+扫码+审批三管齐下,能显著减少浪费。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。