鸿蒙App 电子病历查看(历史就诊记录/处方药提醒)【玩转华为云】
【摘要】 一、引言在医疗信息化快速发展的今天,电子病历(Electronic Medical Record, EMR)已成为医疗机构和患者的核心数据资产。然而,传统电子病历系统普遍存在跨机构数据孤岛、患者访问不便、用药依从性低等问题。鸿蒙操作系统凭借其分布式软总线、安全可信执行环境(TEE)、跨设备协同等核心能力,为构建安全、便捷的电子病历查看与用药提醒系统提供了理想的解决方案。本文聚焦鸿蒙App中实...
一、引言
二、技术背景
1. 鸿蒙医疗健康能力支撑
-
分布式数据管理:实现跨设备(手机、平板、智慧屏)的电子病历数据同步,患者在不同终端均可安全访问病历。 -
安全可信执行环境(TEE):提供硬件级加密存储,确保敏感的病历数据(如诊断结果、用药史)不被恶意窃取。 -
后台任务调度:支持精准的用药提醒服务,即使App在后台或设备重启后仍能可靠触发。 -
权限管理体系:细粒度的数据访问控制,确保患者只能查看本人的病历,医护人员需额外授权才能访问患者数据。 -
健康服务集成:与鸿蒙健康服务深度整合,可将用药记录同步至健康数据中心,形成完整的健康档案。
2. 电子病历数据结构
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3. 核心业务挑战
-
数据安全合规:需符合《个人信息保护法》《健康医疗数据安全指南》《HIPAA》(如涉及国际业务)等法规要求。 -
跨机构数据共享:不同医院使用不同的HIS(医院信息系统)、EMR系统,数据格式与接口各异,需要实现标准化对接。 -
实时性与可靠性:用药提醒需确保高可靠性(99.9%以上触发率),避免因系统故障导致患者漏服药物。 -
用户体验优化:医疗数据专业性强,需通过可视化手段(如图表、时间轴)降低患者理解门槛。
三、应用使用场景
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
四、不同场景下详细代码实现
场景1:历史就诊记录查看与可视化
技术要点
-
使用关系型数据库(RelationalStore) 存储结构化的病历数据(就诊记录、处方)。 -
通过分布式数据对象实现跨设备同步(如手机查看后,平板自动同步最新记录)。 -
采用时间轴组件可视化展示就诊历史,提升用户体验。
1. 病历数据模型定义(MedicalRecord.ets)
// MedicalRecord.ets
/**
* 患者基本信息
*/
export class PatientInfo {
patientId: string; // 患者唯一标识(如身份证号哈希)
name: string; // 姓名
gender: 'male' | 'female' | 'other';
birthDate: string; // YYYY-MM-DD
idCard: string; // 加密存储的身份证号
medicalInsuranceNo: string; // 医保卡号(加密)
allergies: string[]; // 过敏史
}
/**
* 单次就诊记录
*/
export class VisitRecord {
visitId: string; // 就诊唯一ID
patientId: string; // 关联患者ID
visitTime: number; // 就诊时间戳(ms)
department: string; // 就诊科室
attendingDoctor: string; // 主治医生
chiefComplaint: string; // 主诉
presentIllness: string; // 现病史
physicalExam: string; // 体格检查
auxiliaryExam: string; // 辅助检查(如"血常规: WBC 10.5×10^9/L")
diagnosis: string[]; // 诊断结论(ICD-10编码)
treatmentPlan: string; // 治疗方案概述
}
/**
* 处方药品信息
*/
export class PrescriptionDrug {
drugId: string; // 药品唯一ID
visitId: string; // 关联就诊ID
drugName: string; // 通用名
tradeName?: string; // 商品名
specification: string; // 规格(如"0.25g*24片")
dosage: string; // 单次用量(如"1片")
frequency: string; // 频次(如"每日三次")
route: string; // 给药途径(口服、注射等)
course: number; // 疗程(天数)
precautions: string; // 注意事项(如"饭后服用,忌辛辣")
doctorSignature: string; // 医生电子签名(Base64编码)
}
/**
* 用药记录(用于提醒与依从性跟踪)
*/
export class MedicationRecord {
recordId: string; // 记录ID
patientId: string; // 患者ID
drugId: string; // 药品ID
scheduledTime: number; // 计划服药时间(ms)
actualTime?: number; // 实际服药时间(ms)
adherenceStatus: 'pending' | 'taken' | 'missed' | 'skipped'; // 依从性状态
doseTaken?: number; // 实际服用剂量(如"1片")
}
2. 病历数据仓库(MedicalRecordRepository.ets)
// MedicalRecordRepository.ets
import relationalStore from '@ohos.data.relationalStore';
import distributedObject from '@ohos.data.distributedData';
import { PatientInfo, VisitRecord, PrescriptionDrug, MedicationRecord } from './MedicalRecord';
import crypto from '@ohos.security.crypto'; // 用于数据加密
export class MedicalRecordRepository {
private static instance: MedicalRecordRepository = new MedicalRecordRepository();
private rdbStore: relationalStore.RdbStore | null = null;
private kvManager: distributedObject.KVManager | null = null;
private kvStore: distributedObject.KVStore | null = null;
private context: Context | null = null;
public static getInstance(): MedicalRecordRepository {
return MedicalRecordRepository.instance;
}
/**
* 初始化数据库与分布式存储
* @param context 应用上下文
*/
async init(context: Context): Promise<boolean> {
this.context = context;
try {
// 1. 初始化关系型数据库(存储病历核心数据)
const rdbConfig: relationalStore.StoreConfig = {
name: 'medical_record.db',
securityLevel: relationalStore.SecurityLevel.S4 // 最高安全级别
};
this.rdbStore = await relationalStore.getRdbStore(context, rdbConfig);
await this.createTables(); // 创建数据表
// 2. 初始化分布式数据对象(用于跨设备同步)
const kvManagerConfig = {
bundleName: 'com.example.medicalrecord',
userInfo: { userId: await this.getCurrentUserId() } // 获取当前登录用户ID
};
this.kvManager = await distributedObject.createKVManager(kvManagerConfig);
const kvStoreConfig = {
name: 'medical_record_kv',
options: { encrypt: true, persist: true, rebuild: false, securityLevel: distributedObject.SecurityLevel.S4 }
};
this.kvStore = await this.kvManager.getKVStore(kvStoreConfig.name, kvStoreConfig.options);
// 3. 注册跨设备数据同步监听
this.kvStore.on('dataChange', distributedObject.SubscribeType.SUBSCRIBE_TYPE_REMOTE, (changeData) => {
this.handleRemoteDataChange(changeData);
});
console.info('MedicalRecordRepository initialized successfully');
return true;
} catch (err) {
console.error(`Initialization failed: ${JSON.stringify(err)}`);
return false;
}
}
/**
* 创建数据库表结构
*/
private async createTables(): Promise<void> {
if (!this.rdbStore) return;
// 患者信息表
const patientSql = `
CREATE TABLE IF NOT EXISTS patient_info (
patient_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
gender TEXT NOT NULL,
birth_date TEXT NOT NULL,
id_card TEXT NOT NULL, -- 加密存储
medical_insurance_no TEXT, -- 加密存储
allergies TEXT -- JSON数组字符串
)
`;
await this.rdbStore.executeSql(patientSql);
// 就诊记录表
const visitSql = `
CREATE TABLE IF NOT EXISTS visit_record (
visit_id TEXT PRIMARY KEY,
patient_id TEXT NOT NULL,
visit_time INTEGER NOT NULL,
department TEXT NOT NULL,
attending_doctor TEXT NOT NULL,
chief_complaint TEXT,
present_illness TEXT,
physical_exam TEXT,
auxiliary_exam TEXT,
diagnosis TEXT, -- JSON数组字符串
treatment_plan TEXT,
FOREIGN KEY (patient_id) REFERENCES patient_info(patient_id)
)
`;
await this.rdbStore.executeSql(visitSql);
// 处方药品表
const prescriptionSql = `
CREATE TABLE IF NOT EXISTS prescription_drug (
drug_id TEXT PRIMARY KEY,
visit_id TEXT NOT NULL,
drug_name TEXT NOT NULL,
trade_name TEXT,
specification TEXT NOT NULL,
dosage TEXT NOT NULL,
frequency TEXT NOT NULL,
route TEXT NOT NULL,
course INTEGER NOT NULL,
precautions TEXT,
doctor_signature TEXT, -- Base64编码
FOREIGN KEY (visit_id) REFERENCES visit_record(visit_id)
)
`;
await this.rdbStore.executeSql(prescriptionSql);
// 用药记录表
const medicationSql = `
CREATE TABLE IF NOT EXISTS medication_record (
record_id TEXT PRIMARY KEY,
patient_id TEXT NOT NULL,
drug_id TEXT NOT NULL,
scheduled_time INTEGER NOT NULL,
actual_time INTEGER,
adherence_status TEXT NOT NULL,
dose_taken TEXT,
FOREIGN KEY (patient_id) REFERENCES patient_info(patient_id),
FOREIGN KEY (drug_id) REFERENCES prescription_drug(drug_id)
)
`;
await this.rdbStore.executeSql(medicationSql);
}
/**
* 加密敏感数据(如身份证号)
* @param plainText 明文
* @returns 密文(Base64编码)
*/
private async encryptData(plainText: string): Promise<string> {
// 实际项目中应使用设备唯一密钥或用户密码派生的密钥
const key = await crypto.generateKey(crypto.CryptoKey.SYMMETRIC, { algorithm: 'AES-256-GCM', keyUsage: ['encrypt', 'decrypt'] });
const encoder = new TextEncoder();
const data = encoder.encode(plainText);
const encrypted = await crypto.encrypt('AES-256-GCM', key, data);
return btoa(String.fromCharCode(...new Uint8Array(encrypted)));
}
/**
* 解密敏感数据
* @param cipherText 密文(Base64编码)
* @returns 明文
*/
private async decryptData(cipherText: string): Promise<string> {
const key = await crypto.generateKey(crypto.CryptoKey.SYMMETRIC, { algorithm: 'AES-256-GCM', keyUsage: ['encrypt', 'decrypt'] });
const decoder = new TextDecoder();
const data = Uint8Array.from(atob(cipherText), c => c.charCodeAt(0));
const decrypted = await crypto.decrypt('AES-256-GCM', key, data);
return decoder.decode(decrypted);
}
/**
* 保存患者基本信息(加密存储)
*/
async savePatientInfo(patient: PatientInfo): Promise<boolean> {
if (!this.rdbStore) return false;
try {
const valueBucket: relationalStore.ValueBucket = {
'patient_id': patient.patientId,
'name': patient.name,
'gender': patient.gender,
'birth_date': patient.birthDate,
'id_card': await this.encryptData(patient.idCard), // 加密身份证号
'medical_insurance_no': patient.medicalInsuranceNo ? await this.encryptData(patient.medicalInsuranceNo) : '',
'allergies': JSON.stringify(patient.allergies)
};
await this.rdbStore.insert('patient_info', valueBucket);
await this.syncPatientInfoToRemote(patient); // 同步到分布式数据库
return true;
} catch (err) {
console.error(`Save patient info failed: ${JSON.stringify(err)}`);
return false;
}
}
/**
* 查询患者的就诊记录(按时间倒序)
*/
async getVisitRecords(patientId: string): Promise<VisitRecord[]> {
if (!this.rdbStore) return [];
try {
const predicates = new relationalStore.RdbPredicates('visit_record');
predicates.equalTo('patient_id', patientId);
predicates.orderByDesc('visit_time');
const resultSet = await this.rdbStore.query(predicates);
const records: VisitRecord[] = [];
while (resultSet.goToNextRow()) {
const record: VisitRecord = {
visitId: resultSet.getString(resultSet.getColumnIndex('visit_id')),
patientId: resultSet.getString(resultSet.getColumnIndex('patient_id')),
visitTime: resultSet.getLong(resultSet.getColumnIndex('visit_time')),
department: resultSet.getString(resultSet.getColumnIndex('department')),
attendingDoctor: resultSet.getString(resultSet.getColumnIndex('attending_doctor')),
chiefComplaint: resultSet.getString(resultSet.getColumnIndex('chief_complaint')),
presentIllness: resultSet.getString(resultSet.getColumnIndex('present_illness')),
physicalExam: resultSet.getString(resultSet.getColumnIndex('physical_exam')),
auxiliaryExam: resultSet.getString(resultSet.getColumnIndex('auxiliary_exam')),
diagnosis: JSON.parse(resultSet.getString(resultSet.getColumnIndex('diagnosis')) || '[]'),
treatmentPlan: resultSet.getString(resultSet.getColumnIndex('treatment_plan'))
};
records.push(record);
}
resultSet.close();
return records;
} catch (err) {
console.error(`Query visit records failed: ${JSON.stringify(err)}`);
return [];
}
}
/**
* 查询就诊对应的处方药品
*/
async getPrescriptionDrugs(visitId: string): Promise<PrescriptionDrug[]> {
if (!this.rdbStore) return [];
try {
const predicates = new relationalStore.RdbPredicates('prescription_drug');
predicates.equalTo('visit_id', visitId);
const resultSet = await this.rdbStore.query(predicates);
const drugs: PrescriptionDrug[] = [];
while (resultSet.goToNextRow()) {
const drug: PrescriptionDrug = {
drugId: resultSet.getString(resultSet.getColumnIndex('drug_id')),
visitId: resultSet.getString(resultSet.getColumnIndex('visit_id')),
drugName: resultSet.getString(resultSet.getColumnIndex('drug_name')),
tradeName: resultSet.getString(resultSet.getColumnIndex('trade_name')),
specification: resultSet.getString(resultSet.getColumnIndex('specification')),
dosage: resultSet.getString(resultSet.getColumnIndex('dosage')),
frequency: resultSet.getString(resultSet.getColumnIndex('frequency')),
route: resultSet.getString(resultSet.getColumnIndex('route')),
course: resultSet.getLong(resultSet.getColumnIndex('course')),
precautions: resultSet.getString(resultSet.getColumnIndex('precautions')),
doctorSignature: resultSet.getString(resultSet.getColumnIndex('doctor_signature'))
};
drugs.push(drug);
}
resultSet.close();
return drugs;
} catch (err) {
console.error(`Query prescription drugs failed: ${JSON.stringify(err)}`);
return [];
}
}
/**
* 同步患者信息到分布式数据库(跨设备)
*/
private async syncPatientInfoToRemote(patient: PatientInfo): Promise<void> {
if (!this.kvStore) return;
const key = `patient_${patient.patientId}`;
const value = JSON.stringify(patient); // 注意:实际同步时需排除敏感字段或确保传输加密
try {
await this.kvStore.put(key, value);
console.info(`Synced patient info: ${key}`);
} catch (err) {
console.error(`Sync patient info to remote failed: ${JSON.stringify(err)}`);
}
}
/**
* 处理远程数据变化(如其他设备更新了病历)
*/
private handleRemoteDataChange(changeData: distributedObject.ChangeData): void {
// 实现数据合并逻辑,避免覆盖本地未同步的修改
changeData.inserted?.forEach(item => {
console.info(`Remote data inserted: ${item.key}`);
// 根据key前缀解析数据类型并更新本地数据库
});
// 类似处理updated和deleted
}
/**
* 获取当前登录用户ID(示例实现)
*/
private async getCurrentUserId(): Promise<string> {
// 实际项目中应从用户认证模块获取
return 'user_123456';
}
}
五、原理解释
1. 数据流转与安全保障
-
数据采集与录入: -
医护人员通过医院内部的HIS/EMR系统录入病历数据。 -
系统通过HL7 FHIR或RESTful API等标准接口,将结构化数据推送到鸿蒙App的后台服务。 -
对于患者手动录入的数据(如症状日记),App在本地进行初步校验后上传。
-
-
本地安全存储: -
App接收到数据后,首先通过 MedicalRecordRepository进行数据加密(如AES-256-GCM),特别是身份证号、医保卡号等敏感信息。 -
加密后的数据被存入关系型数据库(RdbStore),利用鸿蒙的安全沙箱机制,确保其他应用无法访问。 -
数据库的 securityLevel设置为S4,这是鸿蒙提供的最高安全等级,存储在TEE中,即使设备被root也无法轻易提取。
-
-
跨设备同步: -
当需要跨设备同步时(如手机与平板),App将需要同步的数据(通常是加密后的或已脱敏的)通过分布式数据对象(KVStore) 进行存储。 -
分布式软总线负责发现附近的设备,并建立安全的加密通道(基于TLS 1.3)。 -
数据在传输过程中再次加密,只有目标设备的KVStore能解密并存储。同步过程对用户无感知,且支持离线缓存。
-
-
数据访问控制: -
App启动时,用户需通过生物识别(指纹/人脸) 或PIN码进行身份认证。 -
每次访问敏感病历数据时,都会进行权限校验。例如,查看“过敏史”需要“高”级别权限,而查看“就诊科室”可能只需“中”级别。 -
医护人员访问患者数据时,需额外通过医院颁发的数字证书进行认证,确保最小权限原则。
-
2. 用药提醒实现原理
-
提醒计划生成: -
当医生开具处方并同步到App后, MedicalRecordRepository会解析处方中的dosage和frequency,结合course,生成一个或多个MedicationRecord(用药记录)。 -
每个 MedicationRecord包含一个精确的scheduledTime(计划服药时间)。
-
-
后台任务调度: -
App使用鸿蒙的后台任务管理框架,为每个 scheduledTime创建一个延迟任务(Delayed Task) 或周期性任务(Periodic Task)。 -
为了确保在设备重启或App被强制关闭后任务仍能执行,关键提醒会使用系统级闹钟(AlarmManager) 或WorkScheduler的 REPEATED_ONCE类型任务,并设置setPersisted(true)。
-
-
提醒触发与交互: -
到达预定时间,系统服务会唤醒App(即使它在后台或被杀死),并执行预定义的提醒逻辑。 -
提醒以通知栏消息、铃声、震动的形式呈现给用户。 -
用户点击通知后,App会打开一个提醒确认界面,用户可以选择“已服用”、“稍后提醒”或“跳过”。 -
用户的操作会更新对应 MedicationRecord的adherenceStatus和actualTime,并将结果同步到本地数据库和远程服务器,供医生和患者本人查看。
-
六、核心特性
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
七、原理流程图
整体架构数据流图
graph TD
subgraph "医院内部系统"
A[HIS/EMR系统] -->|HL7 FHIR/API| B(医疗数据中台)
end
subgraph "鸿蒙生态"
B -->|安全数据同步| C{鸿蒙App (手机/平板)}
C --> D[分布式数据对象 KVStore]
C --> E[关系型数据库 RdbStore (TEE)]
C --> F[后台任务调度 WorkScheduler]
C --> G[UI层 (就诊记录/提醒)]
H[智慧屏/手表] -->|分布式软总线| D
D --> H
end
subgraph "用户交互"
G -->|查看/确认| I[患者]
J[医护人员] -->|授权访问| G
end
style A fill:#f9f,stroke:#333,stroke-width:2px
style C fill:#bbf,stroke:#333,stroke-width:2px
style E fill:#bfb,stroke:#333,stroke-width:2px
style I fill:#f96,stroke:#333,stroke-width:2px
用药提醒时序图
sequenceDiagram
participant Doctor as 医生 (HIS)
participant Backend as 医疗数据中台
participant App as 鸿蒙App
participant System as 鸿蒙系统服务
participant User as 患者
Doctor->>Backend: 开具电子处方
Backend->>App: 推送处方数据 (加密)
App->>App: 解析处方,生成用药计划 (MedicationRecord)
App->>System: 创建后台定时任务 (WorkScheduler)
Note over System: 任务持久化,设备重启后仍有效
loop 每日定时检查
System->>System: 到达预定服药时间
System->>App: 唤醒App,触发提醒
App->>User: 显示通知/响铃/震动
User->>App: 点击通知,选择"已服用"
App->>App: 更新MedicationRecord.adherenceStatus = 'taken'
App->>Backend: 同步服药记录 (加密)
end
八、环境准备
1. 开发环境
-
IDE: DevEco Studio 3.1+ (API Version 9+) -
Language: ArkTS (推荐) 或 JavaScript/TypeScript -
SDK: 安装以下能力集: -
@ohos.data.relationalStore -
@ohos.data.distributedData -
@ohos.net.connection -
@ohos.sensors(用于获取步数等,可选) -
@ohos.backgroundTaskManager(用于后台任务) -
@ohos.security.crypto(用于数据加密) -
@ohos.multimodalinput.inputDevice(用于生物识别)
-
-
设备: HarmonyOS 3.0+ 的真机(手机/平板),以便测试分布式能力和后台任务。
2. 权限配置 (module.json5)
{
"module": {
"name": "entry",
"type": "entry",
"description": "Medical Record App",
"mainElement": "EntryAbility",
"deviceTypes": ["phone", "tablet"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ts",
"description": "Main Ability",
"icon": "$media:icon",
"label": "Medical Record",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}
]
}
],
"extensionAbilities": [
{
"name": "ReminderAbility",
"type": "service", // 用于后台提醒的ServiceExtensionAbility
"srcEntry": "./ets/reminder/ReminderAbility.ts"
}
],
"reqPermissions": [
{
"name": "ohos.permission.READ_HEALTH_DATA",
"reason": "读取健康相关数据以关联分析",
"usedScene": { "when": "always" }
},
{
"name": "ohos.permission.WRITE_HEALTH_DATA",
"reason": "写入用药记录至健康中心",
"usedScene": { "when": "always" }
},
{
"name": "ohos.permission.DISTRIBUTED_DATASYNC",
"reason": "跨设备同步电子病历",
"usedScene": { "when": "always" }
},
{
"name": "ohos.permission.USE_BIOMETRIC",
"reason": "使用指纹/人脸进行身份认证",
"usedScene": { "when": "inuse" }
},
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
"reason": "确保用药提醒在后台正常运行",
"usedScene": { "when": "background" }
},
{
"name": "ohos.permission.SCHEDULE_EXACT_ALARM",
"reason": "精确安排用药提醒时间",
"usedScene": { "when": "inuse" }
}
]
}
}
3. 后端服务对接(模拟)
// utils/MockBackendService.ets
import { VisitRecord, PrescriptionDrug } from '../models/MedicalRecord';
export class MockBackendService {
/**
* 模拟从服务器获取就诊记录
*/
static async fetchVisitRecords(patientId: string): Promise<VisitRecord[]> {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 500));
// 返回模拟数据
return [
{
visitId: 'visit_20240520_01',
patientId: patientId,
visitTime: new Date('2024-05-20T09:00:00').getTime(),
department: '心血管内科',
attendingDoctor: '张伟主任',
chiefComplaint: '反复胸闷、气短2周,加重1天。',
presentIllness: '患者2周前无明显诱因出现胸闷,位于心前区,呈压榨性,持续约5分钟,休息后可缓解。...',
physicalExam: 'BP: 145/90mmHg, P: 88次/分, 神清,双肺呼吸音清...',
auxiliaryExam: '心电图: 窦性心律,ST-T改变;心肌酶谱: CK-MB 35U/L (↑)。',
diagnosis: ['不稳定型心绞痛', '高血压1级'],
treatmentPlan: '1. 阿司匹林肠溶片 100mg qd;2. 阿托伐他汀钙片 20mg qn;3. 低盐低脂饮食,注意休息。'
},
{
visitId: 'visit_20231015_02',
patientId: patientId,
visitTime: new Date('2023-10-15T14:30:00').getTime(),
department: '内分泌科',
attendingDoctor: '李娜医生',
chiefComplaint: '多饮、多尿、体重下降3个月。',
presentIllness: '患者3个月前无明显诱因出现口渴、多饮,日饮水量约3000ml,伴尿量增多...',
physicalExam: 'BMI: 22.5kg/m², 神清,心肺腹未见明显异常。',
auxiliaryExam: '空腹血糖: 8.5mmol/L (↑);糖化血红蛋白: 7.8% (↑)。',
diagnosis: ['2型糖尿病'],
treatmentPlan: '1. 盐酸二甲双胍片 0.5g tid;2. 糖尿病饮食,适量运动。'
}
];
}
/**
* 模拟从服务器获取处方详情
*/
static async fetchPrescriptionDrugs(visitId: string): Promise<PrescriptionDrug[]> {
await new Promise(resolve => setTimeout(resolve, 300));
if (visitId === 'visit_20240520_01') {
return [
{
drugId: 'drug_001',
visitId: visitId,
drugName: '阿司匹林肠溶片',
tradeName: '拜阿司匹灵',
specification: '100mg*30片',
dosage: '1片',
frequency: '每日一次',
route: '口服',
course: 30,
precautions: '餐后服用,注意观察有无牙龈出血、皮肤瘀斑等不良反应。',
doctorSignature: 'data:image/png;base64,...'
},
{
drugId: 'drug_002',
visitId: visitId,
drugName: '阿托伐他汀钙片',
tradeName: '立普妥',
specification: '20mg*7片',
dosage: '1片',
frequency: '每晚一次',
route: '口服',
course: 30,
precautions: '睡前服用,定期复查肝功能。',
doctorSignature: 'data:image/png;base64,...'
}
];
}
return [];
}
}
九、实际详细应用代码示例实现
主页面(病历查看主页,Index.ets)
// pages/Index.ets
import { PatientInfo } from '../models/MedicalRecord';
import { MedicalRecordRepository } from '../repository/MedicalRecordRepository';
import { MockBackendService } from '../utils/MockBackendService';
import router from '@ohos.router';
@Entry
@Component
struct Index {
@State patientInfo: PatientInfo | null = null;
@State visitRecords: VisitRecord[] = [];
@State isLoading: boolean = true;
private repo: MedicalRecordRepository = MedicalRecordRepository.getInstance();
aboutToAppear(): void {
this.loadData();
}
async loadData(): Promise<void> {
this.isLoading = true;
const context = getContext(this) as Context;
await this.repo.init(context);
// 模拟从后端获取患者信息和病历
const mockPatientId = 'patient_12345';
const fetchedRecords = await MockBackendService.fetchVisitRecords(mockPatientId);
// 保存到本地数据库
for (const record of fetchedRecords) {
// 实际项目中,这里会调用repo的save方法
// await this.repo.saveVisitRecord(record);
}
// 从本地数据库查询(此处简化为使用mock数据)
this.visitRecords = fetchedRecords.sort((a, b) => b.visitTime - a.visitTime);
// 构建患者信息(模拟)
this.patientInfo = {
patientId: mockPatientId,
name: '王芳',
gender: 'female',
birthDate: '1985-08-12',
idCard: '1101**********123X', // 显示为脱敏形式
medicalInsuranceNo: '****123456789',
allergies: ['青霉素', '海鲜']
};
this.isLoading = false;
}
@Builder
visitTimelineItem(record: VisitRecord) {
Row() {
// 时间点标记
Column() {
Text(new Date(record.visitTime).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }))
.fontSize(14)
.fontColor('#007DFF')
Text(new Date(record.visitTime).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }))
.fontSize(12)
.fontColor(Color.Gray)
}
.alignItems(HorizontalAlign.Center)
.margin({ right: 15 })
// 连接线
if (this.visitRecords.indexOf(record) !== this.visitRecords.length - 1) {
Line()
.width(2)
.height(40)
.stroke(Color.Blue)
.opacity(0.3)
.alignSelf(ItemAlign.Center)
} else {
Blank().height(40)
}
// 内容卡片
Column() {
Text(`[${record.department}] ${record.diagnosis.join(', ')}`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 5 })
Text(`主诉: ${record.chiefComplaint}`)
.fontSize(14)
.fontColor(Color.Gray)
.margin({ bottom: 5 })
Text(`医生: ${record.attendingDoctor}`)
.fontSize(12)
.fontColor(Color.Gray)
}
.padding(10)
.backgroundColor('#F5F8FF')
.borderRadius(8)
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.alignItems(VerticalAlign.Top)
.margin({ bottom: 20 })
}
build() {
Column({ space: 20 }) {
// Header
Row() {
Text('我的病历')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Blank()
Button('用药提醒')
.fontSize(14)
.backgroundColor('#007DFF')
.fontColor(Color.White)
.onClick(() => {
router.pushUrl({ url: 'pages/ReminderPage' });
})
}
.width('100%')
.padding({ top: 20, left: 20, right: 20 })
if (this.isLoading) {
Column() {
LoadingProgress()
.width(50)
.height(50)
.color(Color.Blue)
Text('正在加载病历...')
.fontSize(16)
.fontColor(Color.Gray)
.margin({ top: 10 })
}
.width('100%')
.flexGrow(1)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
} else if (this.patientInfo && this.visitRecords.length > 0) {
// 患者信息卡片
Column() {
Row() {
Image($r('app.media.ic_profile'))
.width(60)
.height(60)
.margin({ right: 15 })
.borderRadius(30)
Column() {
Text(this.patientInfo.name)
.fontSize(20)
.fontWeight(FontWeight.Bold)
Text(`${this.patientInfo.gender === 'female' ? '女' : '男'} | ${this.patientInfo.birthDate}`)
.fontSize(14)
.fontColor(Color.Gray)
}
}
.alignItems(VerticalAlign.Center)
.width('100%')
Divider().margin({ top: 15, bottom: 10 })
Text('过敏史')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.width('100%')
.textAlign(TextAlign.Start)
Row({ space: 10 }) {
ForEach(this.patientInfo.allergies, (allergy: string) => {
Text(allergy)
.fontSize(12)
.backgroundColor(Color.Red)
.fontColor(Color.White)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(10)
})
}
.margin({ top: 5 })
}
.width('100%')
.padding(20)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 10, color: '#1F000000', offsetX: 0, offsetY: 2 })
// 就诊记录时间轴
Column({ space: 10 }) {
Text('就诊记录')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Start)
.margin({ left: 10, top: 10, bottom: 10 })
ForEach(this.visitRecords, (record: VisitRecord) => {
this.visitTimelineItem(record)
}, (record: VisitRecord) => record.visitId)
}
.layoutWeight(1)
.width('100%')
} else {
// 空状态
Column() {
Image($r('app.media.ic_empty'))
.width(100)
.height(100)
.margin({ bottom: 20 })
Text('暂无病历记录')
.fontSize(18)
.fontColor(Color.Gray)
}
.width('100%')
.flexGrow(1)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
}
.width('100%')
.height('100%')
.backgroundColor('#F0F2F5')
}
}
用药提醒页面与后台服务(ReminderPage.ets & ReminderAbility.ts)
// pages/ReminderPage.ets
import { PrescriptionDrug, MedicationRecord } from '../models/MedicalRecord';
import { MockBackendService } from '../utils/MockBackendService';
import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';
@Entry
@Component
struct ReminderPage {
@State medications: MedicationRecord[] = [];
aboutToAppear(): void {
this.loadMedications();
}
async loadMedications() {
// 模拟加载当天的用药计划
const mockDrug = (await MockBackendService.fetchPrescriptionDrugs('visit_20240520_01'))[0];
const now = new Date();
this.medications = [
{
recordId: 'med_001',
patientId: 'patient_12345',
drugId: mockDrug.drugId,
scheduledTime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0, 0).getTime(), // 今天早上8点
adherenceStatus: 'pending',
drugName: mockDrug.drugName,
dosage: mockDrug.dosage
},
{
recordId: 'med_002',
patientId: 'patient_12345',
drugId: mockDrug.drugId,
scheduledTime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 20, 0, 0).getTime(), // 今天晚上8点
adherenceStatus: 'pending',
drugName: mockDrug.drugName,
dosage: mockDrug.dosage
}
].sort((a, b) => a.scheduledTime - b.scheduledTime);
}
build() {
Column({ space: 15 }) {
Text('今日用药计划')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 10 })
List() {
ForEach(this.medications, (med: MedicationRecord) => {
ListItem() {
Row() {
Column() {
Text(`${new Date(med.scheduledTime).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`)
.fontSize(18)
.fontColor(med.adherenceStatus === 'pending' ? '#007DFF' : Color.Gray)
Text(`${med.drugName} ${med.dosage}`)
.fontSize(16)
.fontColor(Color.Black)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
if (med.adherenceStatus === 'pending') {
Button('已服用')
.fontSize(14)
.backgroundColor('#4CAF50')
.fontColor(Color.White)
.onClick(() => this.confirmAdherence(med.recordId, 'taken'))
Button('稍后提醒')
.fontSize(14)
.backgroundColor('#FFC107')
.fontColor(Color.White)
.margin({ left: 10 })
.onClick(() => this.snoozeReminder(med.recordId))
} else if (med.adherenceStatus === 'taken') {
Text('✓ 已服用')
.fontSize(16)
.fontColor('#4CAF50')
.fontWeight(FontWeight.Bold)
} else if (med.adherenceStatus === 'missed') {
Text('! 已错过')
.fontSize(16)
.fontColor(Color.Red)
.fontWeight(FontWeight.Bold)
}
}
.padding(15)
.backgroundColor(Color.White)
.borderRadius(10)
.width('100%')
}
}, (med: MedicationRecord) => med.recordId)
}
.layoutWeight(1)
.width('100%')
.divider({ strokeWidth: 1, color: '#F0F0F0' })
}
.width('100%')
.height('100%')
.backgroundColor('#F0F2F5')
}
confirmAdherence(recordId: string, status: 'taken' | 'skipped') {
// 更新UI状态
const index = this.medications.findIndex(m => m.recordId === recordId);
if (index !== -1) {
this.medications[index].adherenceStatus = status;
// TODO: 更新本地数据库和远程服务器
console.info(`Medication ${recordId} marked as ${status}`);
}
}
snoozeReminder(recordId: string) {
// 实际项目中,这里会取消当前提醒,并创建一个新的10分钟后触发的提醒
console.info(`Snoozing reminder for ${recordId}`);
// 示例:取消后台任务
// backgroundTaskManager.cancelSuspendDelay(recordId);
}
}
// ets/reminder/ReminderAbility.ts
import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';
import Want from '@ohos.app.ability.Want';
export default class ReminderAbility extends Ability {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
console.info('ReminderAbility onCreate');
// 处理启动Ability的want,例如解析提醒ID
}
onDestroy(): void {
console.info('ReminderAbility onDestroy');
}
// 其他生命周期方法和后台任务处理逻辑
}
十、运行结果
-
主页面 (Index.ets): -
顶部显示患者姓名、性别、出生日期和过敏史标签。 -
下方是清晰的就诊记录时间轴,最新的就诊记录在最上方,每项记录卡片显示科室、诊断、主诉和医生信息。 -
点击右上角“用药提醒”按钮,可跳转至提醒页面。
-
-
用药提醒页面 (ReminderPage.ets): -
以列表形式展示当天的用药计划,包括具体时间和药品名称。 -
对于待服用的药品,提供“已服用”和“稍后提醒”按钮。 -
用户操作后,按钮状态变为“✓ 已服用”或“! 已错过”(模拟)。
-
-
数据加载状态: -
应用启动时,显示加载动画和“正在加载病历...”文本。 -
若没有病历数据,则显示空状态图标和“暂无病历记录”提示。
-
-
控制台日志: -
可以看到数据库初始化、数据同步、用药记录更新等操作的日志输出,便于调试。
-
十一、测试步骤以及详细代码
1. 功能测试
-
病历查看: -
启动App,确认能成功加载并展示模拟的就诊记录时间轴。 -
检查患者信息(姓名、性别、过敏史)是否正确显示。 -
点击时间轴上的不同记录,验证UI布局是否正常(后续可扩展为点击进入详情页)。
-
-
用药提醒: -
进入“用药提醒”页面,确认当天的用药计划已列出。 -
点击“已服用”按钮,观察按钮状态是否变为“✓ 已服用”。 -
点击“稍后提醒”按钮,观察控制台是否有相应日志输出。
-
2. 跨设备同步测试
-
前提: 准备两台登录同一华为账号的鸿蒙设备(如手机和平板),并开启蓝牙和Wi-Fi。 -
步骤: -
在手机A上安装并登录App,查看一条就诊记录。 -
在平板B上登录同一账号的App。 -
观察平板B的App是否能在短时间内(通常<30秒)自动获取到手机A上已查看或更新的病历数据,而无需手动刷新。
-
-
验证点: 分布式数据对象(KVStore)是否成功同步了 patient_info或visit_record的变更。
3. 后台提醒测试
-
模拟提醒: 由于精确控制系统闹钟进行测试较复杂,可在代码中临时将 scheduledTime设置为当前时间的几秒钟之后。// 在 ReminderPage.ets 的 loadMedications 方法中修改 scheduledTime: new Date().getTime() + 10000, // 10秒后 -
步骤: -
将App切到后台或锁屏。 -
等待10秒左右,观察设备是否弹出通知栏提醒。 -
点击通知,验证是否能正确跳转到提醒确认界面。
-
-
验证点: WorkScheduler或AlarmManager是否能穿透后台限制,可靠地触发提醒。
4. 自动化测试脚本(示例)
hypium测试框架编写UI自动化脚本。// tests/uitest/MedicalRecord.test.ets
import { describe, it, expect } from 'hypium';
import { AbilityDelegatorRegistry } from '@ohos.application.abilityDelegatorRegistry';
import { UIAbility } from '@ohos.app.ability.UIAbility';
export default function medicalRecordTests() {
describe('Medical Record App Tests', function () {
it('Test Case Description: Verify main page loads and displays patient info', 0, async function () {
// 启动被测应用
let want = {
bundleName: 'com.example.medicalrecord',
abilityName: 'EntryAbility'
};
await AbilityDelegatorRegistry.getAbilityDelegator().startAbility(want);
// 等待页面加载
await driver.delayMs(2000);
// 验证页面标题
let title = await driver.findElementByText('我的病历');
expect(title).assertExist();
// 验证患者姓名
let patientName = await driver.findElementByText('王芳');
expect(patientName).assertExist();
// 验证过敏史标签
let allergy = await driver.findElementByText('青霉素');
expect(allergy).assertExist();
});
// 更多测试用例...
});
}
十二、部署场景
-
医院内部部署 (On-Premise): -
架构: 医院自建机房或私有云部署医疗数据中台和鸿蒙App的后台管理服务。App通过医院内网或VPN与后台通信。 -
优点: 数据完全自主可控,安全性最高,可深度定制。 -
缺点: 初期投入大,运维成本高,难以实现跨院数据共享。
-
-
区域医疗云部署: -
架构: 由卫健委或第三方服务商建设和运营的区域医疗云平台,多家医院接入。App对接区域平台API。 -
优点: 实现区域内医院间的病历互联互通,患者可在授权下跨院调阅病历,促进分级诊疗。 -
缺点: 依赖云服务商的稳定性和安全性,需签署严格的数据托管协议。
-
-
混合云部署: -
架构: 核心敏感数据(如电子病历原文)存储在医院本地的私有云,而脱敏数据、AI分析服务等部署在公有云。 -
优点: 兼顾数据安全与云计算的弹性扩展能力,是当前大型医疗机构的主流选择。
-
-
纯SaaS部署: -
架构: App及其后台服务全部由第三方SaaS提供商运营,医院按需订阅服务。 -
优点: 零运维,快速上线,成本最低。 -
缺点: 数据主权归服务商所有,对服务商的信任度要求极高,适合小型诊所或对数据敏感性要求不极致的机构。
-
十三、疑难解答
|
|
|
|
|---|---|---|
|
|
2. 网络不稳定或防火墙阻挡。 3. KVStore的 securityLevel或加密配置不一致。4. 数据冲突解决策略不当(如同时修改同一条记录)。 |
2. 确保设备在同一局域网内,或能访问公网。 3. 统一各端代码的KVStore配置。 4. 在 handleRemoteDataChange中实现基于时间戳或版本号的合并策略,必要时提示用户手动选择。 |
|
|
2. 未申请 KEEP_BACKGROUND_RUNNING或相关权限被拒。3. 系统省电策略限制了后台任务。 4. 提醒时间设置错误(如时区问题)。 |
2. 检查 module.json5权限声明,并在运行时动态申请。3. 在App的 onBackground生命周期中调用backgroundTaskManager.requestSuspendDelay请求延迟挂起。4. 使用UTC时间戳存储和比较时间。 |
|
|
2. 加密算法的参数(如IV向量)不匹配。 3. 存储或传输过程中数据损坏。 |
2. 对于GCM等模式,确保IV的正确生成和存储。 3. 增加数据完整性校验(如CRC或HMAC)。 |
SecurityLevel错误 |
|
SecurityLevel.S4的功能。2. 在代码中捕获异常,并为不支持的环境提供降级方案(如使用较低安全级别或内存存储)。 |
|
|
2. IP白名单、API Key等认证失败。 3. 数据格式不符合约定(如日期格式、字段缺失)。 |
2. 与医院信息科紧密合作,获取正确的认证信息和接口文档。 3. 在后台服务增加严格的入参校验和数据清洗逻辑。 |
十四、未来展望
技术趋势
-
AI驱动的健康洞察: 结合鸿蒙的NPU(神经网络处理器)能力,在设备端直接运行轻量级AI模型,对患者的病历数据进行实时分析,主动预警潜在健康风险(如“根据您的血糖趋势,未来3个月患糖尿病并发症的风险增加,请及时就医”),实现真正的预防医学。 -
全场景智慧康养: 电子病历将与智能家居、智能座舱、智能穿戴深度融合。例如,汽车在检测到驾驶员心率异常时,可根据其电子病历中的过敏史,自动联系急救中心并发送关键信息;智能冰箱可根据用户的慢性病病历,提醒其避免食用禁忌食物。 -
区块链赋能的可信档案: 利用区块链技术不可篡改的特性,将关键医疗行为(如处方开具、诊断确认、数据访问)上链存证,构建患者主导的、跨机构互信的终身电子健康档案,彻底解决数据确权与追溯难题。 -
多模态交互: 未来可通过语音、手势甚至眼神与病历系统进行交互,方便老年用户或在双手被占用的情况下(如正在做饭)安全地查阅信息或确认用药。
挑战
-
数据标准的统一: HL7 FHIR虽已成为国际标准,但在国内落地仍需时间。不同厂商、不同地区的医院系统异构性极强,数据治理成本高昂。 -
商业模式与利益分配: 跨机构数据共享涉及复杂的利益博弈,如何建立可持续的商业模式,激励各方参与,是推广的关键。 -
伦理与法律风险: AI辅助诊断的责任归属、基因数据等超敏感信息的利用边界、深度伪造技术对病历真实性的威胁等,都对现有的法律体系和伦理框架提出了严峻挑战。
十五、总结
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)