智能随访 v10 业务逻辑设计

范围: 业务流程 / 用户角色 / 数据状态机 / 异常处理 / 入组研究协议 配套: ui-prototype-v10.md (UI 设计) + prototype/index.html (可点击原型) 目标: 把"采集设备 + 后端 + 多患者"的隐性逻辑显性化, 实施前对齐医院/课题组/医生认知


1. 角色定义

角色 主要职责 在系统里能做什么
科室医生 主治大夫 决定哪些患者入组, 看趋势/异常, 调整方案 看数据趋势 / 导出报告 / 标注异常
病房护士 执行采集人 给病房患者轮流测量, 同一台手表多人用 切换门诊号 / 连蓝牙 / 触发测量 / 看实时值
患者 被采集对象 戴表 / 配合手动测血压等 不直接操作小程序 (一般是护士在操作)
患者家属 居家代采集 出院后给患者持续采集 (例如妈妈给俩娃) 输入门诊号 / 连蓝牙 / 触发测量
课题研究员 负责入组的研究人员 设计研究协议, 拉数据做统计 /api/data?patientNo= 拉数据 / 导出
运维管理员 你 + 客户运营 配置设备 / 维护服务 / 处理故障 数据库管理 / 后端部署 / 用户权限

关键认知: 主要操作者不是患者, 是医护。这决定了 UX 必须照顾"医护流水线"场景 (快速切换患者, 实时看连接状态, 不能丢数据)。


2. 核心场景 (Use Cases)

场景 A: 门诊一次性采集 (单人)

1. 患者 A 进诊室
2. 护士拿出手表给患者戴上 (或患者自己已戴)
3. 护士拿出手机扫体验版二维码进小程序
4. 输入患者 A 的门诊号 100234
5. 点 "设备扫描" → 选 S101 → 连接
6. 让患者按表测血压 (或表自动测心率)
7. 数据自动同步入库
8. 护士看一眼首页 "今日采集" 确认有数据
9. 摘表给下一个患者

难点: 切换到下一患者时, 必须切换门诊号. v9 已解决: 点"切换患者"输新号即可.

场景 B: 病房床旁连续采集 (一台手表 N 个患者)

1. 护士 8:00 巡房, 同一台手表
2. 给患者 B (床位 1, 门诊号 100235): 戴 → 测 → 摘
3. 切换门诊号 100236 → 给患者 C: 戴 → 测 → 摘
4. ... 一轮巡房 8 个患者
5. 最后回护士站把数据汇总查看

难点:

场景 C: 居家随访 (家属代采集)

1. 患者 D 出院, 表一并带走
2. 家属 (例如妈妈给俩娃) 用自己微信扫码进小程序
3. 输入孩子 A 门诊号 100237 → 测一周
4. 切换孩子 B 门诊号 100238 → 测一周
5. 数据周期性自动同步
6. 医生在后台 (Tab 3 数据视图) 看趋势

难点:

场景 D: 入组研究 (定期采集 + 协议)

1. 课题组定义研究协议:
   "高血压研究: 每位患者每天 2 次血压 (早 7:00 / 晚 19:00),
    持续 14 天, 完成率 ≥ 80% 算有效样本"
2. 入组 50 位患者, 每人门诊号唯一
3. 系统应能告诉运营:
   - 当前哪些患者按时测了 (今日完成 X/50)
   - 哪些超过 24 小时没测了 (掉队 Y 人)
   - 哪些数据异常 (高血压2级 N 例, 需医生干预)
4. 研究结束后导出整组数据做统计分析

难点:


3. 数据状态机

3.1 设备连接状态机

                        ┌──────────────┐
                  ╔════►│  未连接       │◄═════╗
                  ║     └──────┬───────┘      ║
                  ║            │              ║ adapter 关 / 用户主动断
                  ║            │ 用户点扫描+选 ║
                  ║            ▼              ║
                  ║     ┌──────────────┐      ║
                  ║     │  连接中       │      ║
                  ║     └──────┬───────┘      ║
                  ║            │              ║
                  ║   12s 超时 │ SDK 回 connection=true
                  ║   /握手失败 │              ║
                  ║            ▼              ║
                  ║     ┌──────────────┐      ║
                  ║     │  握手中       │      ║
                  ║     └──────┬───────┘      ║
                  ║            │              ║
                  ║   5s VPDevice 空 │ type=1 回包 (含 MAC/版本)
                  ║            │              ║
                  ║            ▼              ║
                  ║     ┌──────────────┐      ║
                  ║     │  已连接       │═════╝
                  ║     └──────┬───────┘
                  ║            │ 90s 无回包 (心跳暗断)
                  ║            │ 或 connection=false
                  ║            ▼
                  ║     ┌──────────────┐
                  ╚═════│  暗断/重连中  │
                        └──────────────┘
                          指数退避 1/2/4s 三次
                          失败 → 提示用户手动重连

3.2 数据归属状态机

saveData() 触发
    │
    ▼
┌────────────────────┐
│ 立即写 storage 队列 │ (任何时候都先入队, 防丢数据)
└────────┬───────────┘
         │
         ▼
    deviceId 已知?
    ┌───┴───┐
    │  否    │  是
    ▼       ▼
┌──────┐  ┌──────────────────┐
│ 留   │  │ 立即 POST /api/    │
│ pending│  │ health-data         │
└──────┘  └────────┬─────────┘
等 type=1     成功? │ 失败?
回包后 flush  ┌──┴──┐
              ▼     ▼
          ┌─────┐ ┌─────────┐
          │ 入库 │ │ 留 pending│
          └─────┘ └────┬────┘
                       │
                  6 重 flush 兜底:
                  - 2h 定时
                  - 23:59 兜底
                  - app.onShow
                  - 网络恢复
                  - BLE 重连
                  - 队列堆 80

3.3 患者归属状态机 (v9 门诊号)

小程序启动
    │
    ▼
读 storage.patientNo
    │
    ▼
patientNo 存在?
    ┌─┴─┐
    否    是
    │     │
    ▼     ▼
首页红警  首页蓝条
"未输入  "当前 100234"
门诊号"
    │     │
    ▼     ▼
点[输入]  点[切换]
    │     │
    └──┬──┘
       ▼
   wx.showModal editable
       │
       ▼
   输入新门诊号 → storage.patientNo = "100235"
       │
       ▼
   后续 saveData payload 带 patientNo: "100235"
       │
       ▼
   服务端 to_chinese_record 写入 record['门诊号']: "100235"

4. 业务规则

4.1 数据归属规则

规则 实施
一台手表一条物理身份 (mac) wearable_device.mac 优先匹配
同手表数据可归属多个门诊号 大 JSON 数组每条带 门诊号 字段
历史无门诊号数据保留 NULL 字段视为"未分组", 不混淆 v9 数据
门诊号格式 客户当前用 10 位数字 (0012865682), 系统不强校验

4.2 异常数据判定规则 (建议在客户端高亮)

指标 异常阈值 风险等级 视觉
血压收缩压 ≥ 180 或 舒张压 ≥ 120 危急 (Crisis) 🔴 红底白字 + 弹提醒
血压收缩压 ≥ 140 或 舒张压 ≥ 90 高血压 2 级 🟠 橙色高亮
血压收缩压 ≥ 130 或 舒张压 ≥ 80 高血压 1 级 🟡 黄色提示
心率 ≥ 120 (静息) 或 ≤ 40 危急 🔴 红色
心率 100-120 (静息) 偏高 🟡 黄色
血氧 ≤ 90 % 危急 🔴 红色
血氧 90-94 % 偏低 🟠 橙色
体温 ≥ 38.5°C 或 ≤ 35°C 异常 🟠 橙色
血糖 ≥ 11.1 mmol/L (餐后) 或 ≤ 3.9 异常 🟠 橙色

当前实施: 服务端 classify_bp 已经做血压风险等级 (写入大 JSON 的 风险等级 字段). 其他指标尚未实施. v10 应补齐.

4.3 入组研究采集规则 (v12 才上, v10 先打基础)

研究协议示例:
  name: "高血压晨晚血压观察"
  duration_days: 14
  daily_tasks:
    - time_window: "07:00-08:30"
      indicator: "bloodPressure"
      times_per_day: 1
      description: "晨起血压"
    - time_window: "19:00-20:30"
      indicator: "bloodPressure"
      times_per_day: 1
      description: "晚间血压"
  completion_threshold: 0.8
  alert_rules:
    - if: "missed_consecutive >= 2"
      action: "通知研究员"
    - if: "高血压2级 数 >= 3 in 7 天"
      action: "通知主治医生"

4.4 数据保留与清理规则

数据类型 保留期限 清理触发
患者真实采集 (含门诊号) 永久 不主动清, 仅手动按门诊号导出后归档
早期种子/演示数据 N/A (应清) 客户允许后调 DELETE /api/device/:id
pending 队列 (storage) MAX_QUEUE=500 LRU 自动丢最旧
调试 ble_event 表 90 天 服务端定时任务 (未实施)

4.5 单台手表多人区分规则


5. 异常处理矩阵

异常 触发条件 用户感知 系统行为 恢复路径
蓝牙没开 adapter unavailable 红色 toast "请打开手机蓝牙" 跳过连接尝试 用户手动开
附近权限没开 (Android 12+) 5s 内 0 设备扫到 modal 引导去权限设置 弹 modal 用户去系统设置开
表被其他手机占用 12s 握手超时 modal "可能被其他手机连着" 重试按钮 用户去那台手机断
iOS pair 残留 第二轮扫描仍 0 个 modal 引导忽略此设备 弹 modal 设置→蓝牙→忽略
弱信号 / 距离过远 RSSI < -75dBm 列表过滤掉 静默 走近表 1 米
网络不通 POST 失败 静默入 pending 6 重兜底 flush 网络恢复自动重传
表换电池 / 重启 connection_lost 状态条变红 自动重连 (指数退避) 1/2/4s 重试
测量值异常 (高血压2级) classify_bp 命中 视觉高亮 写入 风险等级 医生在 Tab 3 看到 + 标记
门诊号填错了 UI 没输入约束 数据照入 (错号) 暂无 (建议加患者列表二次校对)
设备没电 表本身关机 扫不到 视为"没开机" 用户充电后重连
同一时刻多次测量 表批量推送 见到 50 条相同步数 服务端原样接收 客户端可去重显示 (UI 层)

6. 权限与角色矩阵

操作 病房护士 家属 医生 课题研究员 运维管理员
输入/切换门诊号
连接蓝牙 / 断开
触发测量 (软件层)
查看本患者趋势
查看跨患者数据 ✅ (本科室) ✅ (本研究组)
标注异常 / 写病历
导出 CSV/PDF
删除数据 ✅ (危险)
配置研究协议
调试 / OTA / 切换服务

当前小程序: 无角色概念, 谁拿到二维码谁能用全部功能. v10 不打算实施角色权限 (太重), 仅在 UI 层把"调试"功能折叠到 Tab 4 末尾, 正式版 IS_TEST_BUILD=false 时隐藏即可.

长期方向: 走微信小程序 wx.login 拿 openid → 服务端建用户表 → 角色与权限. 但客户当前不需要, 暂不做.


7. 数据接入与导出对接

7.1 当前已有

7.2 v10 应补齐

接口 用途
GET /api/patients 列出所有出现过的门诊号 + 各自数据条数 + 最近活动时间 (Tab 3 患者切换 chip)
GET /api/data?patientNo=xxx&from=xxx&to=xxx&type=bloodPressure 时间窗 + 指标过滤 (Tab 3 趋势图)
GET /api/export/csv?patientNo=xxx 服务端生成 CSV 下载链接 (Tab 3 导出)
GET /api/abnormal?patientNo=xxx&days=7 服务端预筛异常数据 (Tab 3 异常列表)

7.3 v12+ 路线图 (不在 v10 范围)


8. 通知与提醒 (v10 仅前端轻量)

通知什么 时机 通道
护士 "未输入门诊号"红警示 进首页 顶部状态条
护士 "蓝牙断了, 数据无法同步" adapter 关 toast
护士 "测量值异常, 风险等级 X" 测量后回包 弹 toast 或 modal
家属 "未上传 X 条" 网络断时 首页角标 (待加)
医生 "异常数据" 后台查看时 Tab 3 高亮

9. 关键决策待用户/客户拍板

# 决策点 选项
1 测量前是否加"门诊号二次确认"? 加 (减少错号) / 不加 (减少打扰)
2 异常数据是否弹 modal 拦截? 拦截 (强制处理) / 仅高亮 (不打扰流程)
3 数据 Tab 趋势图用什么库? ECharts (功能强但接入麻烦) / F2 (蚂蚁出品, 小程序原生) / canvas 手画
4 是否在 v10 加"患者列表"概念? 加 (能看到所有患者, 切换更方便) / 不加 (沿用 storage.patientNo 单值)
5 "调试"功能是否在 Tab 4 完全隐藏 (正式版)? 完全隐藏 / 显示但加密码进入 / 始终可见
6 是否考虑离线缓存模式? 加 (野外能用) / 不加 (依赖网络)
7 入组研究协议引擎是否纳入 v10? 纳入 (大工程) / 留 v12 (推荐)
8 数据导出格式? CSV (Excel 友好) / JSON (开发者友好) / PDF (医生友好) / 全要

10. 实施优先级建议

v10 必做 (跟着 4-tab UI 一起)

  1. 4-tab 信息架构 (UI)
  2. 顶部患者条 + 连接条 (沿用 v10)
  3. 异常数据视觉高亮 (血压风险等级渲染颜色)
  4. 数据 Tab 趋势图 (ECharts/F2)
  5. 数据 Tab CSV 导出
  6. 调试入口折叠

v10 增强 (有时间就做)

  1. 测量前门诊号二次确认 (5 秒倒计时)
  2. 首页 "未上传 X 条" 角标
  3. 患者切换历史 (近 5 个门诊号快速切回)
  4. 网络恢复 toast "已上传 X 条"

v12 (留作下一版)

  1. 患者列表概念 + 增删改
  2. 入组研究协议引擎
  3. 医生端 Web 后台
  4. 角色权限模型
  5. HIS 对接