⚠️ 不良事件业务规则文档
1总体概览
▼不良事件模块是护理漏测系统的第5个业务模块,数据从轻流平台同步到 MySQL,再通过报表页面展示统计分析结果。
轻流平台 (care.yckycn.com)
│
└── openApi ──────── 不良事件 (adverse_events)
└─ AppKey: dtonmhf02801
└─ 脚本: sync_adverse_events.py
└─ 认证: Header accessToken(不放URL参数)
│
▼
MySQL (nursing.adverse_events)
│
┌───────┴───────┐
│ │
报表页面 基本信息
adverse-events- resident_info(入住协议)
report.html 通过 id_number JOIN
| 模块 | 说明 |
|---|---|
| 数据源 | 轻流 openApi,AppKey: dtonmhf02801 |
| 认证方式 | Header 传 accessToken,永久 Token 共用 |
| 同步脚本 | /opt/leakcheck/sync_adverse_events.py |
| 目标表 | MySQL nursing.adverse_events(32 个业务字段) |
| 报表页面 | adverse-events-report.html,路由 /adverse-events-report |
| 定时同步 | 每日凌晨 03:00 全量同步 |
| 当前数据量 | 约 149 条记录 |
| API 每页上限 | 200 条(openApi 限制,传 500 也只返回 200) |
2数据源与同步规则
▼2.1 API 调用方式
| 项目 | 值 |
|---|---|
| 请求地址 | POST https://care.yckycn.com/openApi/app/dtonmhf02801/apply/filter |
| 认证方式 | Header: accessToken: 187f968c-48a4-4874-95bc-edb084a4083d |
| Content-Type | application/json |
| 分页参数 | pageNum 从1开始, pageSize 最大200 |
⚠️ 关键踩坑:accessToken 必须放在 Header 中,不能放在 URL 参数里!之前护理/评估用的 view API 是放 URL,但 openApi 必须用 Header。
2.2 同步模式
ℹ️ 不良事件为永久保留数据,始终采用全量同步模式,不做增量、不清理旧数据。
| 模式 | 说明 |
|---|---|
| 全量(唯一模式) | 不设日期过滤,拉取轻流所有记录;同步后删除轻流中已不存在的记录,确保 DB 与轻流完全一致 |
2.3 数据一致性保障
每次同步完成后,会比对本次拉取到的所有 apply_id,删除 DB 中存在但轻流中已不存在的记录:
DELETE FROM adverse_events WHERE apply_id NOT IN (本次拉取的所有apply_id)
✅ 这样确保DB 数据与轻流业务表完全一致:轻流新增的会插入,轻流修改的会更新,轻流删除的也会从 DB 删除。
2.4 数据写入策略
- 使用
INSERT ... ON DUPLICATE KEY UPDATE(upsert 模式) - 唯一键:
apply_id(轻流申请ID),同一条记录重复同步会覆盖更新 - 每次同步记录
sync_time字段 - 自动从
event_time截取event_date(yyyy-mm-dd)存入独立字段
2.5 脏数据防护
- 跳过
event_date在未来超过1天的异常记录 - 若 API 返回的 apply_id 列表为空,跳过删除操作(防止误删全表)
2.6 定时任务
| 任务 | Cron 表达式 | 说明 |
|---|---|---|
| 全量同步 | 00 3 * * * | 每日凌晨 3:00 执行 |
2.7 手动同步
- 命令行:
python3 sync_adverse_events.py - 页面:同步工具页面 → "不良事件同步" 按钮(调用
/api/adverse-events/sync)
3MySQL 表结构
▼表名:adverse_events,引擎 InnoDB,字符集 utf8mb4_unicode_ci
| 字段名 | 类型 | 说明 | 索引 |
|---|---|---|---|
| id | INT AUTO_INCREMENT | 自增主键 | PRIMARY |
| apply_id | BIGINT | 轻流申请ID | UNIQUE |
| id_number | VARCHAR(18) | 身份证号码(关联键) | INDEX |
| event_number | VARCHAR(100) | 编号 | |
| caregiver_name | VARCHAR(100) | 长者姓名 | |
| institution | VARCHAR(200) | 机构名称 | INDEX |
| area | VARCHAR(200) | 休养区 | INDEX |
| care_level | VARCHAR(50) | 护理等级 | |
| gender | VARCHAR(10) | 性别 | |
| age | VARCHAR(10) | 年龄 | |
| birth_date | VARCHAR(20) | 出生日期 | |
| bed_number | VARCHAR(100) | 床位号 | |
| admission_date | VARCHAR(20) | 入住日期 | |
| id_file_no | VARCHAR(100) | 档案编号 | |
| event_type | VARCHAR(100) | 不良事件类型 | INDEX |
| event_time | VARCHAR(50) | 事发时间(精确到分钟) | |
| event_date | VARCHAR(20) | 事发日期(yyyy-mm-dd) | INDEX |
| event_shift | VARCHAR(200) | 事发班次(原始值) | |
| event_location | VARCHAR(200) | 事发地点 | |
| event_activity | TEXT | 事发时长者活动(多值逗号分隔) | |
| event_resident_status | TEXT | 事发时长者状态(多值逗号分隔) | |
| staff_status | VARCHAR(200) | 事发时工作人员状态 | |
| staff_years | VARCHAR(50) | 员工年限 | |
| restraint_used | VARCHAR(100) | 约束具使用情况 | |
| restraint_agreement | VARCHAR(50) | 约束协议是否签订 | |
| fall_grade | VARCHAR(100) | 跌倒分级 | |
| pipe_type | VARCHAR(100) | 管路类型 | |
| severity_class | VARCHAR(200) | 严重程度分类 | |
| damage_level | VARCHAR(200) | 给长者造成损害的程度 | |
| weight | VARCHAR(50) | 体重(KG) | |
| past_history | TEXT | 既往史 | |
| apply_time | VARCHAR(50) | 申请时间 | |
| update_time | VARCHAR(50) | 更新时间 | |
| flow_status | VARCHAR(50) | 当前流程状态 | |
| sync_time | DATETIME | 同步时间 | INDEX |
💡 设计说明:当前报表用不到的字段(既往史、档案编号、严重程度分类、损害程度、跌倒分级等)也全量存储,为后续功能预留。
4字段映射(queId → 数据库列)
▼轻流表单中每个字段对应一个 queId,同步脚本将 queId 映射到 MySQL 列名。
| queId | 轻流字段名 | MySQL 列名 | 备注 |
|---|---|---|---|
208906738 | 身份证号码 | id_number | 🔑 关联键 |
0 | 编号 | event_number | |
208906730 | 长者姓名 | caregiver_name | |
208906622 | 机构名称 | institution | |
208906703 | 休养区 | area | |
208906965 | 护理等级 | care_level | |
208906629 | 性别 | gender | |
208906958 | 年龄 | age | |
208906957 | 出生日期 | birth_date | |
208906664 | 床位号 | bed_number | |
208906966 | 入住日期 | admission_date | |
208907423 | 档案编号 | id_file_no | 预留 |
208908821 | 不良事件类型 | event_type | 跌倒/坠床/非计划性拔管 |
208915924 | 事发时间 | event_time | 精确到分钟 |
208940856 | 事发班次 | event_shift | 原始值含岗位+时段 |
208940857 | 事发地点 | event_location | |
208940858 | 事发时长者活动 | event_activity | 🔀 多值字段 |
208940859 | 事发时长者状态 | event_resident_status | 🔀 多值字段 |
208940862 | 事发时工作人员状态 | staff_status | 🔀 多值字段 |
208940863 | 员工年限 | staff_years | |
208940855 | 约束具使用情况 | restraint_used | |
208907428 | 约束协议是否签订 | restraint_agreement | |
208940860 | 跌倒分级 | fall_grade | 仅跌倒类有值 |
208940861 | 管路类型 | pipe_type | 仅拔管类有值,新字段 |
208907441 | 严重程度分类 | severity_class | 预留 |
208907442 | 给长者造成损害的程度 | damage_level | 🔀 多值字段,预留 |
208907425 | 体重(KG) | weight | |
208907084 | 既往史 | past_history | 预留 |
2 | 申请时间 | apply_time | 从 answers 外提取 |
3 | 更新时间 | update_time | 从 answers 外提取 |
4 | 当前流程状态 | flow_status | 从 answers 外提取 |
5报表指标取数规则
▼报表通过 /api/adverse-events/report-data 接口获取统计数据,以下为各指标的取数规则。
| # | 指标 | API Key | 数据源字段 | 统计方式 | 展示范围 |
|---|---|---|---|---|---|
| 1 | 总事件数 | total | event_type | 对非空 event_type 计数求和 | 全部 |
| 2 | 按事件类型 | by_type | event_type | GROUP BY event_type,COUNT 降序 | 全部 |
| 3 | 事发时间分段 | by_period | event_time | 提取小时 → 映射时段名 → COUNT(见第6节) | 全部 |
| 4 | 事发班次 | by_shift | event_shift | 按原始值 GROUP BY 分组统计(不再归一化) | 全部 |
| 5 | 事发地点 TOP5 | by_location | event_location | GROUP BY event_location,COUNT 降序 | 前5名 |
| 6 | 长者活动 TOP5 | by_activity | event_activity | 拆分多值 → 单项 COUNT 降序(见第8节) | 前5名 |
| 7 | 长者状态 TOP5 | by_resident_status | event_resident_status | 拆分多值 → 单项 COUNT 降序(见第8节) | 前5名 |
| 8 | 工作人员状态 TOP5 | by_staff_status | staff_status | 拆分多值 → 单项 COUNT 降序(见第8节) | 前5名 |
| 9 | 员工年限 | by_staff_years | staff_years | GROUP BY staff_years,COUNT 降序 | 全部 |
| 10 | 约束具使用 | by_restraint | restraint_used | GROUP BY restraint_used,COUNT 降序 | 全部 |
| 11 | 约束协议签订 | by_restraint_agreement | restraint_agreement | GROUP BY restraint_agreement,COUNT 降序 | 全部 |
| 12 | 跌倒分级 | by_fall_grade | fall_grade | 仅 event_type='跌倒' 时统计 | 全部 |
| 13 | 管路类型 | by_pipe_type | pipe_type | 仅 event_type='非计划性拔管' 时统计 | 全部 |
| 14 | 严重程度分类 | by_severity | severity_class | GROUP BY severity_class,COUNT 降序 | 全部 |
| 15 | 损害程度 | by_damage | damage_level | 拆分多值 → 单项 COUNT 降序(见第8节) | 全部 |
💡 专项过滤说明:
- 跌倒分级:只统计
event_type = '跌倒'的记录,其他事件类型不展示此指标 - 管路类型:只统计
event_type = '非计划性拔管'的记录
6事发时间分段规则
▼从 event_time 字段提取小时(0-23),映射到固定时段名称。映射在 _segment_time_period() 函数中实现。
| 时段名称 | 时间范围 | 小时值 | 示例 |
|---|---|---|---|
| 意外高发 | 2:00 - 4:59 | 2, 3, 4 | 04:30 → 意外高发 |
| 晨间护理 | 5:00 - 6:59 | 5, 6 | 06:15 → 晨间护理 |
| 早餐 | 7:00 - 7:59 | 7 | 07:30 → 早餐 |
| 上午活动 | 8:00 - 10:59 | 8, 9, 10 | 09:45 → 上午活动 |
| 午餐 | 11:00 - 11:59 | 11 | 11:20 → 午餐 |
| 午休 | 12:00 - 13:59 | 12, 13 | 13:00 → 午休 |
| 下午活动 | 14:00 - 16:59 | 14, 15, 16 | 15:30 → 下午活动 |
| 晚餐 | 17:00 - 17:59 | 17 | 17:45 → 晚餐 |
| 晚间护理 | 18:00 - 19:59 | 18, 19 | 19:00 → 晚间护理 |
| 睡眠 | 20:00 - 1:59 | 0, 1, 20, 21, 22, 23 | 22:30 → 睡眠 |
💡 时段划分暂固定不变,后续如需调整再沟通修改
_segment_time_period() 函数即可。
代码逻辑
def _segment_time_period(hour):
if 2 <= hour <= 4: return "意外高发"
elif hour in (5, 6): return "晨间护理"
elif hour == 7: return "早餐"
elif 8 <= hour <= 10: return "上午活动"
elif hour == 11: return "午餐"
elif hour in (12, 13): return "午休"
elif 14 <= hour <= 16: return "下午活动"
elif hour == 17: return "晚餐"
elif hour in (18, 19): return "晚间护理"
else: return "睡眠" # 20-23, 0-1
7班次取值规则
▼轻流中 event_shift 字段原始值包含岗位和时段信息,v2.1起按原始值直接分组统计,不再做归一化合并。
变更说明(v2.1):此前按关键字归一化为"白班/夜班/其他班次"3种,导致不同岗位的班次被错误合并(如"护士:白班"和"照护师:白班"混在一起)。现在按原始值分别统计,保留完整的岗位+班次信息。
当前数据分布
| 原始值 | 数量 |
|---|---|
| 护士:白班(8:31-17:30) | 62 |
| 护士:夜班(17:31-8:30) | 54 |
| 照护师:白班(6:31-18:30) | 3 |
| 护士早交接班时段 | 2 |
| 照护师:夜班(18:31-6:30) | 2 |
| 其他:早上06:03分 | 1 |
| 医生/药剂师/康复师:白班(8:31-17:30) | 1 |
| 护士晚交接班时段 | 1 |
| 护理员早交接班时段 | 1 |
SQL逻辑(v2.1)
SELECT event_shift as name, COUNT(*) as cnt FROM adverse_events WHERE event_shift IS NOT NULL AND event_shift != '' GROUP BY event_shift ORDER BY cnt DESC
8多值字段拆分规则
▼部分字段在轻流中为多选,存储时用英文逗号拼接,统计时需拆分为单项分别计数。
涉及字段
| 字段 | MySQL列 | 存储格式 | 统计方式 |
|---|---|---|---|
| 事发时长者活动 | event_activity | "上下床,如厕,坐下" | 拆分为 3 条单项各 +1 |
| 事发时长者状态 | event_resident_status | "坐轮椅,使用助行器" | 拆分为 2 条单项各 +1 |
| 事发时工作人员状态 | staff_status | "护理其他长者,巡视中" | 拆分为 2 条单项各 +1 |
| 给长者造成损害的程度 | damage_level | "擦伤,淤青" | 拆分为 2 条单项各 +1 |
拆分逻辑
# 以事发时长者活动为例
for r in cur.fetchall():
for a in r['event_activity'].split(','):
a = a.strip()
if a: act_cnt[a] = act_cnt.get(a, 0) + 1
⚠️ 注意:拆分后各项计数之和可能大于总记录数(一条记录可能包含多个活动项)。
举例说明
| 记录 | event_activity 原始值 | 拆分后计数 |
|---|---|---|
| #1 | "上下床,如厕" | 上下床 +1, 如厕 +1 |
| #2 | "上下床" | 上下床 +1 |
| #3 | "坐下,如厕,站立" | 坐下 +1, 如厕 +1, 站立 +1 |
| 统计结果 | 上下床:2, 如厕:2, 坐下:1, 站立:1 | |
9筛选条件逻辑
▼报表页面提供3个筛选条件,所有条件为 AND 关系,空值表示"不限制"。
| 筛选条件 | 前端控件 | 对应字段 | SQL 条件 | 数据来源 |
|---|---|---|---|---|
| 机构名称 | 下拉框 | institution | WHERE institution = ? | org_area_mapping 表 |
| 休养区 | 下拉框(联动) | area | WHERE area = ? | org_area_mapping 表(按选中机构过滤) |
| 统计周期 | 日期区间选择器 | event_date | WHERE event_date >= ? AND event_date <= ? | 用户手动选择 |
机构/休养区取值逻辑
✅ 与护理/评估页面保持一致,从
org_area_mapping 表获取机构-休养区对应关系。
- API:
/api/leak/org-areas(共用,非不良事件独有) - 优先从
org_area_mapping表查询(排除名称含"测试"的机构) - 如果
org_area_mapping为空,降级从resident_info表查询(排除离院) - 机构下拉变化时,休养区下拉联动刷新
日期区间说明
- 关联字段:
event_date(从 event_time 截取的 yyyy-mm-dd) - 开始日期:包含(
>=) - 结束日期:包含(
<=) - 默认值:无(不选则不限制日期范围)
10基本信息关联规则
▼报表中需要的老人基本信息(姓名、机构、休养区、护理等级等),统一通过身份证号关联入住协议获取。
关联方式
adverse_events resident_info
───────────── ─────────────
id_number ────── JOIN ──────► id_number
├── caregiver_name (姓名)
├── institution (机构)
├── area (休养区)
├── care_level (护理等级)
├── status (状态:在院/离院)
└── ...
关联规则
- 关联键:
adverse_events.id_number = resident_info.id_number - 数据来源优先级:入住协议(resident_info)> 不良事件表自身字段
- 原因:入住协议是最新、最准的基础信息,且每日定时同步更新
- 与护理/评估一致:之前护理漏测和评估漏评均采用此关联方式
⚠️ 当前实现说明:目前报表查询直接使用 adverse_events 表中的 institution/area/care_level 字段进行筛选和展示(因为不良事件表本身包含这些字段且与 resident_info 一致)。如果后续需要关联 resident_info 获取更准确的信息(如离院状态过滤),需改用 JOIN 查询。
数据验证
| 来源 | 姓名 | 机构 | 休养区 | 护理等级 |
|---|---|---|---|---|
| 不良事件表 | 李琴琴 | 梅苑颐养中心 | 颐养楼2区4层 | 3 |
| resident_info | 李琴琴 | 梅苑颐养中心 | 颐养楼2区4层 | 3 |
| ✅ 交叉验证一致 | ||||
── 文档结束 · 不良事件业务规则 v2.1 · 2026-06-08 ──