资讯详情

鸿蒙医院陪诊系统开发实战:ArkTS与分布式数据对象实现流程拆解

📅 2026/10/18 16:00:30 | 华诺云谱 👁 阅读
鸿蒙医院陪诊系统开发实战:ArkTS与分布式数据对象实现流程拆解
简介这份资源是面向HarmonyOS初学者与移动应用开发者的医院陪诊系统源码包基于HarmonyOS SDK 6.0.0、Stage模型与ArkTS语言实现适合作为轻应用开发练手项目或课程设计参考。系统覆盖用户注册、登录、病情选择与陪诊确认等核心流程采用纯文字交互并适配手机与平板设备通过页面路由、状态管理和本地数据模拟完成无后端的数据暂存。压缩包共12个文件约10KB以json/json5配置、ts逻辑代码、html页面及png图标、md说明文档为主结构清晰涵盖用户数据模型、应用入口管理及各功能页面。目前已有141人学习。读者可从中获取完整的项目目录组织方式、编译运行指南与常见问题解决方案并了解持久化存储、网络请求对接和样式优化等扩展方向便于快速上手鸿蒙应用开发与二次迭代。1. 鸿蒙医院陪诊系统从挂号到取药的流程拆解与代码落地医院陪诊这件事真正跑过一遍的人都知道痛点不在“陪”而在“等”和“找”。患者从进门诊大厅开始挂号、候诊、检查、缴费、取药每一步都可能卡在信息不对称上。鸿蒙医院陪诊系统要解决的核心问题是把这些离散环节串成一条可追踪的服务链路让陪诊人员、患者、后台调度三方看到同一份进度。它适合两类人一是想用 HarmonyOS 做垂直行业应用的开发者二是医疗机构里负责服务流程数字化的技术人员。代码层面这套系统通常拆成患者端、陪诊端、调度后台三个模块用 ArkTS 写 UI用分布式数据对象做跨设备状态同步。下面按实际落地顺序把关键环节和代码骨架讲清楚。2. 鸿蒙陪诊系统的技术选型与工程结构2.1 为什么用 ArkTS 分布式数据对象而不是纯后端轮询陪诊场景有一个硬约束患者和陪诊人员可能在不同设备上但需要看到几乎同步的进度。常见做法是后端 WebSocket 推消息前端轮询拉状态。这套方案在鸿蒙上不是不行但多了一层网络往返弱网环境下进度条会“跳变”。HarmonyOS 提供的分布式数据对象Distributed Data Object允许同一应用在不同设备间同步内存对象配合 ArkTS 的状态管理进度变更可以做到近实时。选型理由有三点。第一陪诊流程的状态字段不多主要是当前环节、下一环节、预计等待时间、陪诊人员位置用分布式对象同步的数据量很小。第二鸿蒙的分布式能力对同一账号下的设备有天然信任关系省去自建长连接的心跳维护。第三ArkTS 的Observed和ObjectLink装饰器能让 UI 自动响应数据变化减少手写 diff 的逻辑。但要注意分布式数据对象不是数据库它不负责持久化。实际项目里我一般会加一层轻量存储用relationalStore把关键节点落盘防止设备重启后状态丢失。2.2 工程目录与模块划分一个可运行的鸿蒙陪诊系统 Demo目录结构大致如下entry/src/main/ets/ ├── entryability/ │ └── EntryAbility.ets ├── pages/ │ ├── PatientHome.ets // 患者端首页 │ ├── EscortTask.ets // 陪诊端任务页 │ └── DispatchBoard.ets // 调度后台看板 ├── model/ │ ├── VisitFlow.ets // 就诊流程状态模型 │ └── TaskOrder.ets // 陪诊工单模型 ├── common/ │ ├── DistributedSync.ets // 分布式同步封装 │ └── TimeUtil.ets // 时间与等待时长计算 └── resources/ └── base/ └── element/ └── string.json模块划分的原则是model只放数据结构和纯函数common放跨页面复用的工具pages只处理 UI 和用户交互。这样拆的好处是分布式同步的逻辑不会散落在各个页面里排查问题时能快速定位是数据层还是 UI 层。2.3 就诊流程状态模型的定义先定义流程状态。医院陪诊的典型环节包括挂号、候诊、就诊、检查、缴费、取药、离院。每个环节有状态未开始、进行中、已完成和预计耗时。// model/VisitFlow.ets export enum FlowStep { REGISTER register, // 挂号 WAITING waiting, // 候诊 CONSULT consult, // 就诊 EXAM exam, // 检查 PAYMENT payment, // 缴费 MEDICINE medicine, // 取药 LEAVE leave // 离院 } export enum StepStatus { PENDING pending, ACTIVE active, DONE done } Observed export class VisitFlow { patientId: string ; currentStep: FlowStep FlowStep.REGISTER; stepStatus: RecordFlowStep, StepStatus { [FlowStep.REGISTER]: StepStatus.PENDING, [FlowStep.WAITING]: StepStatus.PENDING, [FlowStep.CONSULT]: StepStatus.PENDING, [FlowStep.EXAM]: StepStatus.PENDING, [FlowStep.PAYMENT]: StepStatus.PENDING, [FlowStep.MEDICINE]: StepStatus.PENDING, [FlowStep.LEAVE]: StepStatus.PENDING }; estimatedWaitMinutes: number 0; lastUpdateTs: number 0; // 推进到下一环节 advanceTo(next: FlowStep): void { this.stepStatus[this.currentStep] StepStatus.DONE; this.currentStep next; this.stepStatus[next] StepStatus.ACTIVE; this.lastUpdateTs Date.now(); } }这段代码的关键点是Observed装饰器。它让VisitFlow实例的属性变化可以被 UI 层感知。advanceTo方法做了两件事把当前环节标记为完成把下一环节标记为进行中。lastUpdateTs用于计算等待时长也用于分布式同步时判断哪台设备的数据更新。参数说明estimatedWaitMinutes不是写死的实际项目里由调度后台根据排队人数动态下发。stepStatus用Record类型而不是数组是为了让每个环节的状态查询是 O(1)避免在 UI 里写循环查找。2.4 分布式同步的封装与调用分布式数据对象的创建和同步需要封装否则每个页面都写一遍distributedDataObject.create会很乱。// common/DistributedSync.ets import distributedDataObject from ohos.data.distributedDataObject; import { VisitFlow } from ../model/VisitFlow; const SESSION_ID visit_flow_session; export class DistributedSync { private static flow: VisitFlow | null null; private static dataObject: distributedDataObject.DataObject | null null; // 初始化并绑定分布式对象 static initFlow(patientId: string): VisitFlow { if (DistributedSync.flow) { return DistributedSync.flow; } const flow new VisitFlow(); flow.patientId patientId; DistributedSync.flow flow; // 创建分布式数据对象绑定到同一 session DistributedSync.dataObject distributedDataObject.create( getContext(this) as Context, flow ); DistributedSync.dataObject.setSessionId(SESSION_ID); // 监听远端变更 DistributedSync.dataObject.on(change, (sessionId, fields) { console.info([DistributedSync] remote change: ${JSON.stringify(fields)}); // 远端字段变化时本地 flow 已由框架自动更新 // 这里可以触发额外的 UI 刷新或日志 }); return flow; } // 主动同步本地变更到远端 static syncFlow(): void { if (DistributedSync.dataObject) { DistributedSync.dataObject.save(visit_flow); } } // 释放资源 static release(): void { if (DistributedSync.dataObject) { DistributedSync.dataObject.off(change); DistributedSync.dataObject null; } DistributedSync.flow null; } }逻辑说明create方法把本地对象包装成分布式对象setSessionId指定同步会话。同一账号下的设备只要加入同一个 session就能收到变更。save方法触发一次主动同步通常在advanceTo之后调用。on(change)是远端变更的回调框架会自动更新本地对象属性回调里主要做日志和辅助刷新。参数说明SESSION_ID建议用业务 ID 拼接比如visit_flow_${patientId}避免不同患者的数据串到一起。getContext(this)在非 UI 文件里需要传入 AbilityContext实际项目里可以通过全局上下文管理类获取。2.5 患者端进度页的最小实现患者端最核心的页面是进度展示。用ObjectLink绑定VisitFlow状态变化时 UI 自动刷新。// pages/PatientHome.ets import { VisitFlow, FlowStep, StepStatus } from ../model/VisitFlow; import { DistributedSync } from ../common/DistributedSync; Entry Component struct PatientHome { State flow: VisitFlow DistributedSync.initFlow(patient_001); build() { Column({ space: 12 }) { Text(当前环节${this.flow.currentStep}) .fontSize(20) .fontWeight(FontWeight.Bold) Text(预计等待${this.flow.estimatedWaitMinutes} 分钟) .fontSize(16) .fontColor(Color.Gray) // 环节列表 ForEach(Object.values(FlowStep), (step: FlowStep) { Row() { Text(step) .fontSize(16) .fontColor(this.flow.stepStatus[step] StepStatus.DONE ? Color.Green : Color.Black) Blank() Text(this.flow.stepStatus[step]) .fontSize(14) .fontColor(Color.Gray) } .width(100%) .padding(8) }) Button(模拟推进到下一环节) .onClick(() { const steps Object.values(FlowStep); const idx steps.indexOf(this.flow.currentStep); if (idx steps.length - 1) { this.flow.advanceTo(steps[idx 1]); DistributedSync.syncFlow(); } }) } .padding(16) .width(100%) } }这段代码里State和ObjectLink的区别要注意State用于组件内部状态ObjectLink用于绑定Observed对象的属性。这里flow是Observed类的实例用State也能触发刷新但更规范的做法是用ObjectLink。实际项目里如果遇到 UI 不刷新的情况先检查装饰器是否配对。参数说明ForEach遍历Object.values(FlowStep)时顺序由枚举定义顺序决定所以枚举的书写顺序要和实际流程一致。advanceTo之后必须调用syncFlow否则远端设备看不到变化。3. 陪诊工单调度与状态流转的代码实现3.1 工单模型与调度队列陪诊系统不只是展示进度还要把患者和陪诊人员匹配起来。工单模型需要包含患者信息、陪诊人员信息、当前状态、优先级。// model/TaskOrder.ets export enum OrderStatus { CREATED created, // 已创建 ASSIGNED assigned, // 已分配 IN_PROGRESS in_progress, // 服务中 COMPLETED completed, // 已完成 CANCELLED cancelled // 已取消 } Observed export class TaskOrder { orderId: string ; patientId: string ; patientName: string ; escortId: string ; status: OrderStatus OrderStatus.CREATED; priority: number 0; // 数值越大优先级越高 createTs: number 0; assignTs: number 0; completeTs: number 0; // 分配陪诊人员 assign(escortId: string): boolean { if (this.status ! OrderStatus.CREATED) { return false; } this.escortId escortId; this.status OrderStatus.ASSIGNED; this.assignTs Date.now(); return true; } // 开始服务 start(): boolean { if (this.status ! OrderStatus.ASSIGNED) { return false; } this.status OrderStatus.IN_PROGRESS; return true; } // 完成服务 complete(): boolean { if (this.status ! OrderStatus.IN_PROGRESS) { return false; } this.status OrderStatus.COMPLETED; this.completeTs Date.now(); return true; } }逻辑说明每个状态变更方法都先校验当前状态防止非法流转。比如未分配的工单不能直接开始服务。这种“状态机”写法在调度系统里很常见能避免很多脏数据。参数说明priority用于插队逻辑急诊患者可以设高优先级。createTs、assignTs、completeTs三个时间戳用于计算响应时长和服务时长是后续做效率分析的基础数据。3.2 调度队列的排序与分配逻辑调度后台需要从等待队列里挑出最合适的工单分配给陪诊人员。排序规则通常是优先级降序同优先级按创建时间升序。// common/DispatchQueue.ets import { TaskOrder, OrderStatus } from ../model/TaskOrder; export class DispatchQueue { private orders: TaskOrder[] []; // 添加工单 add(order: TaskOrder): void { this.orders.push(order); this.sort(); } // 排序优先级降序创建时间升序 private sort(): void { this.orders.sort((a, b) { if (a.priority ! b.priority) { return b.priority - a.priority; } return a.createTs - b.createTs; }); } // 取出下一个待分配工单 next(): TaskOrder | null { const idx this.orders.findIndex( o o.status OrderStatus.CREATED ); if (idx -1) { return null; } return this.orders[idx]; } // 分配并移除 assignNext(escortId: string): TaskOrder | null { const order this.next(); if (order order.assign(escortId)) { this.orders this.orders.filter(o o.orderId ! order.orderId); return order; } return null; } // 查看队列长度 size(): number { return this.orders.filter(o o.status OrderStatus.CREATED).length; } }逻辑说明sort方法在每次添加工单后调用保证队列有序。next方法只查找状态为CREATED的工单已分配的不再参与调度。assignNext把分配和移除合并成一个原子操作避免并发场景下同一工单被分配给两个人。参数说明findIndex返回第一个匹配项因为队列已排序所以取到的就是优先级最高且最早创建的工单。如果队列很大可以考虑用堆结构优化但陪诊场景的并发量通常不大数组排序足够。3.3 陪诊端任务列表的渲染与操作陪诊人员打开应用后需要看到分配给自己的工单并能操作“开始服务”和“完成服务”。// pages/EscortTask.ets import { TaskOrder, OrderStatus } from ../model/TaskOrder; Entry Component struct EscortTask { State myOrders: TaskOrder[] []; aboutToAppear(): void { // 模拟从调度后台拉取当前陪诊人员的工单 const order new TaskOrder(); order.orderId ORD_20240101_001; order.patientId patient_001; order.patientName 模拟患者A; order.escortId escort_001; order.status OrderStatus.ASSIGNED; order.createTs Date.now() - 60000; order.assignTs Date.now() - 30000; this.myOrders.push(order); } build() { List({ space: 10 }) { ForEach(this.myOrders, (order: TaskOrder) { ListItem() { Column({ space: 8 }) { Text(工单${order.orderId}) .fontSize(16) .fontWeight(FontWeight.Bold) Text(患者${order.patientName}) .fontSize(14) Text(状态${order.status}) .fontSize(14) .fontColor(order.status OrderStatus.IN_PROGRESS ? Color.Blue : Color.Gray) Row({ space: 10 }) { Button(开始服务) .enabled(order.status OrderStatus.ASSIGNED) .onClick(() { if (order.start()) { this.myOrders [...this.myOrders]; } }) Button(完成服务) .enabled(order.status OrderStatus.IN_PROGRESS) .onClick(() { if (order.complete()) { this.myOrders [...this.myOrders]; } }) } } .padding(12) .width(100%) .backgroundColor(Color.White) .borderRadius(8) } }) } .padding(16) .width(100%) } }逻辑说明aboutToAppear里模拟了从后台拉取工单的过程实际项目里换成网络请求。按钮的enabled属性根据工单状态动态控制防止误操作。order.start()和order.complete()返回布尔值只有状态合法时才返回 true然后通过重新赋值数组触发 UI 刷新。参数说明this.myOrders [...this.myOrders]这种写法是为了让 ArkTS 感知数组变化。直接push有时不会触发刷新展开运算符创建新数组更稳妥。3.4 调度后台看板的数据聚合调度后台需要看到全局数据等待队列长度、进行中工单数、平均等待时长。这些指标用简单的聚合函数就能算出来。// common/DashboardStats.ets import { TaskOrder, OrderStatus } from ../model/TaskOrder; export interface DashboardData { waitingCount: number; inProgressCount: number; completedCount: number; avgWaitMinutes: number; } export function calcDashboard(orders: TaskOrder[]): DashboardData { const waiting orders.filter(o o.status OrderStatus.CREATED); const inProgress orders.filter(o o.status OrderStatus.IN_PROGRESS); const completed orders.filter(o o.status OrderStatus.COMPLETED); // 平均等待时长已分配工单的 assignTs - createTs const assigned orders.filter(o o.assignTs 0); let totalWait 0; assigned.forEach(o { totalWait (o.assignTs - o.createTs); }); const avgWaitMinutes assigned.length 0 ? Math.round(totalWait / assigned.length / 60000) : 0; return { waitingCount: waiting.length, inProgressCount: inProgress.length, completedCount: completed.length, avgWaitMinutes }; }逻辑说明calcDashboard是纯函数输入工单数组输出看板数据。这种写法便于单元测试也方便在页面里用State绑定结果。平均等待时长只统计已分配的工单未分配的还在队列里不计入。参数说明60000是毫秒转分钟的系数。Math.round取整避免出现小数分钟。实际项目里可以再加一个“最长等待时长”指标用于预警。4. 鸿蒙陪诊系统落地避坑与常见问题排查4.1 分布式同步不生效远端设备看不到状态变化现象患者端推进了环节陪诊端进度条没动日志里也没有change回调。原因最常见的是setSessionId没调用或者两台设备登录的账号不一致。分布式数据对象要求同一账号下的设备才能同步。另一个原因是save方法没调用本地变更没有触发同步。解决先检查setSessionId是否在create之后立即调用。再确认两台设备登录的是同一个华为账号。最后在advanceTo之后加一行DistributedSync.syncFlow()。如果还不生效在on(change)回调里打日志确认回调是否触发。4.2 ArkTS 状态刷新不及时按钮点击后 UI 没反应现象点击“开始服务”按钮工单状态在日志里已经变了但页面上的文字没更新。原因ArkTS 的状态管理依赖装饰器和赋值方式。如果直接修改对象属性而没有触发Observed的监听或者数组用push而不是重新赋值UI 不会刷新。解决对于Observed类的属性变更确保类实例被ObjectLink或State持有。对于数组用this.myOrders [...this.myOrders]重新赋值。对于嵌套对象考虑用Observed加ObjectLink的组合而不是只在外层加State。4.3 工单状态非法流转导致数据错乱现象一个已经完成的工单又被分配了一次或者未分配的工单直接变成了“服务中”。原因状态变更方法没有做前置校验或者调用方跳过了校验直接改status字段。解决所有状态变更必须走assign、start、complete这些方法方法内部先判断当前状态是否允许流转。禁止在业务代码里直接写order.status OrderStatus.IN_PROGRESS。如果确实需要强制改状态比如后台管理员操作单独写一个forceUpdateStatus方法并加日志。4.4 等待时长计算出现负数或异常大值现象看板上的平均等待时长是负数或者几万分钟。原因createTs和assignTs的赋值时机不对或者设备时间不同步。分布式场景下不同设备的时间可能有偏差。解决时间戳统一由服务端下发客户端不要自己生成createTs。如果必须客户端生成用Date.now()并确保设备时间自动同步。计算等待时长时加一个保护Math.max(0, assignTs - createTs)。4.5 页面销毁后分布式对象未释放导致内存泄漏现象反复进出患者端页面内存占用持续上升日志里出现重复的change回调。原因DistributedSync.initFlow每次调用都创建新的分布式对象但页面销毁时没有调用release。解决在页面的aboutToDisappear生命周期里调用DistributedSync.release()。如果多个页面共用同一个 flow用引用计数管理最后一个页面销毁时才释放。实际项目里我一般把DistributedSync做成单例并在 Ability 的onDestroy里统一释放。5. 陪诊流程的进阶技巧用时间片做等待时长预测等待时长预测是陪诊系统里最能体现“智能”的部分。简单做法是写死一个固定值但实际排队人数是动态的。我一般用时间片统计法把过去一段时间的实际等待时长按小时切片每个小时算一个平均值预测时取当前小时的历史均值。// common/WaitPredictor.ets export class WaitPredictor { // 按小时存储历史等待时长分钟 private history: Mapnumber, number[] new Map(); // 记录一次实际等待 record(hour: number, minutes: number): void { if (!this.history.has(hour)) { this.history.set(hour, []); } this.history.get(hour)!.push(minutes); } // 预测当前小时的等待时长 predict(hour: number): number { const list this.history.get(hour); if (!list || list.length 0) { return 15; // 默认值 } const sum list.reduce((a, b) a b, 0); return Math.round(sum / list.length); } // 清理过期数据只保留最近 7 天 prune(maxRecordsPerHour: number 50): void { this.history.forEach((list, hour) { if (list.length maxRecordsPerHour) { this.history.set(hour, list.slice(-maxRecordsPerHour)); } }); } }逻辑说明record在每次工单完成时调用传入当前小时和实际等待分钟数。predict取当前小时的历史均值作为预测值。prune防止数据无限增长每个小时最多保留 50 条记录。参数说明hour用 0 到 23 的整数。默认值 15 分钟是经验值实际项目里可以根据医院科室调整。maxRecordsPerHour设 50 是平衡精度和内存太小则预测波动大太大则旧数据拖累响应。这个方案的好处是不依赖复杂模型纯统计就能跑起来。坑在于冷启动阶段数据少预测不准。我的习惯是前三天用默认值同时后台记录实际等待等数据够了再切到预测模式。另外节假日和周末的排队规律和工作日不同如果医院周末也开诊最好把hour的 key 加上星期几变成dayOfWeek_hour的组合。还有一个细节预测值不要直接展示给患者而是展示一个区间比如“预计等待 10 到 20 分钟”。这样即使预测有偏差患者也不会觉得系统在骗人。区间上下限可以用历史数据的 25 分位和 75 分位来算比均值更稳健。最后说一个我踩过的坑早期版本把预测逻辑放在客户端每台设备各算各的结果同一时刻不同患者看到的等待时长不一样投诉很多。后来改成服务端统一计算客户端只负责展示问题才解决。如果要做分布式预测这种全局性指标一定要收敛到一处计算不要分散。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑