ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

公共医疗CRM落地:从患者档案到筛查随访闭环与HIS集成实践

公共医疗CRM落地:从患者档案到筛查随访闭环与HIS集成实践 简介面向公共医疗卫生行业的微软Dynamics客户关系管理解决方案白皮书从新医改政策背景切入梳理了医疗卫生机构面临的市场机遇、运营挑战与竞争压力适合医疗信息化从业者、医院管理者及客户关系管理项目实施人员参考。文档聚焦患者关系管理阐明如何通过整合患者数据、建立完整档案配合自动化预约、诊疗、处方及康复计划流程实现差异化服务提升患者满意度与品牌形象。内容覆盖医疗客户关系管理的核心工作流如患者管理、预约调度、病历记录、治疗计划跟踪、患者沟通与反馈收集等并结合医院与诊所场景给出定制化应用实例。还分析了微软此类方案的产品优势包括灵活性、可扩展性、云端与本地部署选择、与Office 365及Power BI等工具的深度集成以及利用数据分析预测资源需求、控制成本、避免过度医疗的价值。资源包为单个PDF文件大小约1.46MB结构清晰按机遇挑战、工作流程、解决方案实例、产品优势与结论等章节递进。已有106人学习适合希望全面理解医疗行业客户关系管理应用逻辑、或正在规划相关信息化建设的人员快速获取体系化参考。1. 公共医疗行业的“关系管理”不等于看病软件用户愿意埋单的是服务闭环很反直觉的一点是公共卫生机构部署 Microsoft Dynamics CRM最常见的失败开头是从挂号、病历、检验单开始做。真正生产环境里这些场景早被 HIS/EMR 覆盖CRM 的价值在“以服务对象为中心”的业务协同层解决的是筛查任务完成率低、高危人群随访失联、机构之间转诊断点、检查结果事后无法追溯这类问题。这个标题要落地的其实是把患者、家庭、机构、服务事件串成可操作的服务闭环。受众是甲方信息科、集成商实施顾问、数据处理工程师下面从实体设计、业务闭环、集成架构、运维验证四条线展开。2. 核心实体与字段设计先划清楚“服务对象”和“患者”的边界很多项目在做公共医疗 CRM 时会把 Contact、Lead、Account 三个实体混着用结果同一个人的数据在联系人、潜在患者、家庭分组里各存一份。公共医疗卫生行业更合适的做法是把 CRM 当成“区域居民健康服务数据库”而不是把业务逻辑硬塞进销售漏斗。我一般会把 Contact 当作常住人口Account 当作家庭或机构Lead 只在社会筛查引入新对象时短暂出现转化后直接并入 Contact避免重复建档。2.1 从 Contact 起步而不是从 Patient 表起步“患者”在公共卫生服务里是一个短期角色一个人可以同时是糖尿病患者、癌症筛查对象、结核病密切接触者。把这些身份拆成多个实体是灾难。Contact 中的出生日期、性别、联系电话、地址就是基础台账不稳定的身份用选项集表示例如新增new_health_role字段而不是另建 Patient 实体。这样既保证一个服务对象只有一条主记录也方便后续按卫生服务包去扩展。常见需求里一个家庭多个成员往往会同时进入同一个慢病管理计划。Account 做家庭主记录Contact 挂到 Account 下后续生成筛查任务时就能按“家庭内一人异常、全家提醒”的规则批量触发。具体实施时把 Account 的relationship_type设置成“家庭”再把联系人主字段填成姓名和身份证件号即可不推荐用自有编号做唯一键因为跨机构合并档案时自有编号经常冲突。2.2 用解决方案管理实体、字段和权限直接在默认解决方案里创建自定义字段是一场运维灾难。正式项目里我会先建一个名为PublicHealth_Base的非托管解决方案把 Contact、Case、Activity 等需要扩展的实体都放进去再在该解决方案里添加字段、关系、选项集。上线时导出为托管解决方案部署到生产环境。基本步骤进入 Power Apps 门户创建解决方案选择 Common Data Service 环境。添加现有实体 Contact并添加自定义字段和选项集。使用“发布自定义项”把变更发布到当前环境。在解决方案导出界面选择“托管”后导出确保生产环境无法直接删除字段。这个流程的好处是兜底字段、状态机、业务规则都跟着解决方案走不同环境之间的差异可以对比。如果直接在默认解决方案里改字段后续做分环境升级时大概率出现“字段已存在但权限配置丢失”的情况。2.3 公共医疗卫生行业最常用的字段设计表实体用途推荐字段说明Contact服务对象主记录new_nhc_id健康档案号、new_health_role角色、guardian_phone监护人电话健康档案号做业务主键但不用它做实体主键Account家庭/机构household_type家庭类型、coverage_zone划片管理区域家庭类型区分独居、纯老人家庭、有儿童家庭Case单次服务事件service_type服务类型、screening_status筛查状态、priority_level风险分级Case 承载筛查、转诊、随访三类工作Phone Call电话随访记录followup_result随访结果、call_duration通话时长与 Case 建立 N:1 关系字段设计的关键在于把“档案信息”和“服务信息”分开。地址、身份证号这类高频读取的字段直接放 Contact而“最近一次血压”“随机血糖”必须放单独的血压/血糖清单或 Case 中。否则每次更新都会覆盖历史记录审计日志里看不到趋势变化。2.4 通过 Web API 快速初始化测试数据的 PowerShell 脚本公共医疗项目通常需要从旧系统迁移一批测试数据。用界面录入几百条不现实我会先用 Web API 做一轮初始化。下面的 PowerShell 脚本用client_credentials获取令牌后向 contacts 实体集写入一条测试服务对象。$tenant $env:CRM_TENANT_ID $clientId $env:CRM_CLIENT_ID $clientSecret $env:CRM_CLIENT_SECRET $crmUrl $env:CRM_ENVIRONMENT_URL # 申请访问令牌scope 必须指向 CRM 环境地址不能用 Graph API 的地址 $tokenUrl https://login.microsoftonline.com/$tenant/oauth2/v2.0/token $tokenBody { client_id $clientId client_secret $clientSecret scope $crmUrl/.default grant_type client_credentials } $token (Invoke-RestMethod -Method Post -Uri $tokenUrl -Body $tokenBody).access_token $headers { Authorization Bearer $token OData-Version 4.0 Content-Type application/json } $contactBody { firstname 张 lastname 三 mobilephone 13800000000 address1_line1 XX街道社区卫生服务中心 new_nhc_id 441900001 new_health_role 100000002 # 选项集的数值成员不是选项文本 } | ConvertTo-Json $uri $crmUrl/api/data/v9.2/contacts try { $response Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -Body $contactBody $response.contactid } catch { $_.ErrorDetails.Message }代码中的client_credentials需要在 Azure AD 注册应用并把这个应用加入 Dynamics 365 环境的安全角色。new_health_role传的是选项集数值如果填成中文文本Web API 会返回InvalidAttributeValue。POST 成功后返回的contactid是 GUID后续所有 Case、Phone Call 都通过这个 GUID 关联不要试图把 Contact 主键换成new_nhc_id。3. 从筛查到随访在 Dynamics CRM 里落地公共卫生服务闭环筛查活动、体检计划、慢病随访在系统里如果只是“建一批联系人”那这个 CRM 项目基本已经失败。真正要落地的是状态驱动从人群筛选、任务分发、结果回收、异常转介到随访完成。CRM 里能承担这个闭环的实体不是 Contact而是 Case。3.1 Case 不只是投诉工单按公卫服务事件来建模很多实施顾问第一次接触公共卫生需求时会把 Case 当作“患者投诉工单”来设计这是误区。Case 在公卫场景应表示“一次需要被追踪的服务事件”例如“糖尿病年度筛查”“肿瘤高危人群电话随访”“出院患者转回社区管理”。Case 的作用是承载事件的状态、责任人、计划时间和实际完成时间而 Contact 只负责描述人本身。建模时给 Case 增加一个new_service_type选项集区分高血压筛查、糖尿病随访、老年人跌倒风险干预等。再增加new_batch_id字段用来标记同一批导入的筛查任务。批号字段在数据修正时非常有用可以直接用它查询“本次导入哪些还没处理”不用依赖创建时间。3.2 服务事件状态机状态和状态原因配置Dynamics CRM 的 Case 自带状态和状态原因默认状态不一定适合公共卫生。我会在解决方案里重新配置状态原因状态值状态原因触发动作Open(1)新分配 / 待联系创建 Case 后自动进入In Progress(2)筛查中 / 转介中 / 随访中筛查结果回填生成待办Resolved(5)已完成 / 已转介更新下次随访计划Canceled(0)重复档案 / 失访记录失访原因释放资源状态和状态原因必须与报表口径一致。报表统计“筛查完成率”时只统计 Resolved 且状态原因为“已完成”的 Case而“转介及时率”看的是从 Open 到 In Progress 中“转介中”的时间差。如果状态原因被随意填写后续 Power BI 报表要对齐大量历史数据代价很高。3.3 用 Power Automate 自动推进状态并发送通知常见做法是当公卫人员填写“筛查结果”字段后系统自动做三件事把 Case 状态改为“进行中”生成下一次随访任务向联系人手机号发送通知。创建云端流时选择“Microsoft Dataverse - 更新记录”作为触发器实体名选择 Case。添加筛选条件new_result等于“异常”。添加“更新记录”动作把状态改为“进行中”。添加“创建活动”动作创建一条 Phone Call关联当前 Case。发送短信用消息网关 HTTP 动作。Power Automate 的 HTTP 动作可以直接操作 Web API下面是一个 JSON 配置示例{ method: PATCH, url: {triggerOutputs()?[body/contacts]}, headers: { Content-Type: application/json, If-Match: * }, body: { statecode: 1, statuscode: 1 } }这里的If-Match: *表示不限制并发版本适合低风险状态变更。statecode和statuscode的数值必须和 3.2 节的表一致。如果修改了状态原因但没同步更新 Power Automate 里的数字动作会直接失败错误信息通常出现在运行历史里。3.4 用 Python 批量提交筛查任务的幂等实现一批 3000 人的筛查名单往往来自 Excel 或 HIS 导出的 CSV直接在界面上逐条创建 Case 会让人崩溃。我更推荐写一个短脚本用 Web API 批量提交并用批号字段做幂等控制。import os import requests tenant_id os.environ[CRM_TENANT_ID] client_id os.environ[CRM_CLIENT_ID] client_secret os.environ[CRM_CLIENT_SECRET] crm_url os.environ[CRM_ENVIRONMENT_URL] token_url fhttps://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token token_data { client_id: client_id, client_secret: client_secret, scope: crm_url /.default, grant_type: client_credentials, } token requests.post(token_url, datatoken_data).json()[access_token] headers { Authorization: fBearer {token}, Content-Type: application/json, } screening_records [ {nhc_id: 441900001, service_type: hypertension, plan_date: 2026-01-10}, {nhc_id: 441900002, service_type: diabetes, plan_date: 2026-01-10}, ] for record in screening_records: payload { new_nhc_id: record[nhc_id], new_service_type: record[service_type], scheduledend: record[plan_date], new_batch_id: SCREEN-20260101, } r requests.post(f{crm_url}/api/data/v9.2/cases, headersheaders, jsonpayload) if r.status_code in (200, 201, 204): print(created, record[nhc_id]) else: print(r.json().get(error, {}).get(message))这里new_batch_id是幂等键。脚本重跑时先按new_batch_id nhc_id查询已有 Case存在就不创建只补更新。scheduledend在 OData 里传 ISO 8601 格式例如2026-01-10T00:00:00Z不能直接传2026-01-10否则空值会导致任务不上日历。4. 与 HIS/EMR 集成数据同步、审计与分布式事务的取舍公共医疗机构的 Dynamics CRM 不可能独立于院内信息科系统存在。常见部署模式是 CRM 作为云端的服务协作平台HIS、EMR、体检系统通过 ESB 或消息网关向 CRM 推送就诊事件CRM 再向检验系统回写筛查结果。只要集成链路里没有一个明确的数据归属规则两边数据就对不上。4.1 谁主谁从把主数据归属写进接口设计首先要定义每条数据的所有权。HIS 负责门诊、住院、检验检查、诊断等临床数据CRM 负责服务对象、任务、状态、随访结果、机构协作数据。同步时Contact 的姓名、身份证号、电话以 HIS 为主健康角色、风险分级、失访标记以 CRM 为主。重叠字段用更新时间戳解决冲突更新时间晚的覆盖早的。这类接口设计里最常见的错误是让 CRM 反写姓名和身份证号到 HIS。机构号、就诊卡号在 HIS 侧是权威数据源一旦反写可能会触发 HIS 的防篡改机制导致接口被锁定。正确做法是 CRM 只回传随访结果、下次计划日期、失访状态这类业务字段。4.2 为什么不在公卫系统里做分布式事务很多人一谈集成就会想起“分布式事务的解决方案”直接用两阶段提交。但公共卫生系统跨越院内网和云平台网络延迟、防火墙策略、数据库隔离级别各不相同。强行做全局分布式事务会把 CRM 的写入操作锁在长事务里HIS 一次断连可能导致整批数据回滚。更实际的是最终一致性方案HIS 把检查结果推送到 Azure Service Bus 或 KafkaCRM 消费者按顺序处理消费成功后才确认消息失败就重试超限进入死信队列。这种模式的好处是即使 HIS 某个时段推送了 10 万条体检记录CRM 也不会被同步请求拖垮。公卫场景不要求秒级一致“当天数据当天对齐”已经足够没必要为强一致付出巨大的运维成本。4.3 用 C# 做 Upsert 的幂等接收下面是一个 C# 控制台或 Azure Function 里常见的Upsert用法通过new_external_id字段查找已有记录存在就更新不存在就创建。public async TaskGuid UpsertCaseFromHis(HttpClient client, HisEventDto eventDto) { // external_id 是 HIS 主键用它做查找字段避免重复创建 var fetchXml $fetch top1 entity namecase attribute namecaseid/ filter condition attributenew_external_id operatoreq value{eventDto.EventId}/ /filter /entity /fetch; var encoded Uri.EscapeDataString(fetchXml); var queryResponse await client.GetAsync($/api/data/v9.2/cases?fetchXml{encoded}); var content await queryResponse.Content.ReadAsStringAsync(); Guid foundId Guid.Empty; if (queryResponse.IsSuccessStatusCode) { using var doc JsonDocument.Parse(content); var values doc.RootElement.GetProperty(value); if (values.GetArrayLength() 0) foundId values[0].GetProperty(caseid).GetGuid(); } var payload new Dictionarystring, object? { [title] eventDto.ScreeningName, [new_external_id] eventDto.EventId, [scheduledstart] eventDto.OccurredAt, [prioritycode] eventDto.PriorityLevel }; if (foundId Guid.Empty) { var createResponse await client.PostAsJsonAsync(/api/data/v9.2/cases, payload); createResponse.EnsureSuccessStatusCode(); var createResult await createResponse.Content .ReadFromJsonAsyncDictionarystring, JsonElement(); return createResult![caseid].GetGuid(); } else { using var request new HttpRequestMessage(HttpMethod.Patch, $/api/data/v9.2/cases({foundId})); request.Headers.Add(If-Match, *); request.Content JsonContent.Create(payload); var updateResponse await client.SendAsync(request); updateResponse.EnsureSuccessStatusCode(); return foundId; } }代码中new_external_id必须建有索引否则随着数据量增大每次查找都会变成全表扫描。scheduledstart传的是 UTC 时间如果 HIS 里的时间是本地时间必须先换算时区再写入否则随访日历会偏移。prioritycode的数值映射关系要提前约定例如 1 是低风险2 是中风险3 是高风险。4.4 敏感字段的字段级权限和审计公共卫生数据涉及身份证号、家庭住址、疾病史不建议把所有患者信息无条件开放给所有角色。在 Dynamics CRM 中可以用字段安全配置文件限制部分字段的读写角色可读字段不可读字段说明社区护士姓名、电话、随访备注身份证号、完整家庭住址电话可读导出受限信息科管理员身份证号随访备注导出时脱敏公共卫生专家完整字段家庭住址仅项目期授权字段级权限配置在“高级设置 安全 字段安全配置”中新建配置文件后把需要控制的字段加入进去。审计日志建议全开在审计设置里勾选敏感实体的“创建、更新、删除”即可。审计数据可以这样用 Web API 查询$fetchXml fetch aggregatetrue entity nameaudit attribute nameobjectid aggregatecount aliastotal/ filter condition attributeaction operatoreq valueUpdate/ condition attributecreatedon operatorlast-x-days value30/ /filter /entity /fetch | ConvertTo-Json $fetchQuery [uri]::EscapeDataString($fetchXml) Invoke-RestMethod -Uri $crmUrl/api/data/v9.2/audits?fetchXml$fetchQuery -Headers $headers这里的audit实体存储的是抽象日志字段里包含objectid和action。要还原具体改了什么字段需要进一步拉取auditdetail。建议在报表层把audit和auditdetail做关联视图而不是每次排错都手动翻界面。5. 上线前的性能验证与运维技巧5.1 用 FetchXML 验证导入完整性批量导入筛查任务后第一件事不是看界面而是用 FetchXML 统计条数。下面这段脚本用来按批号统计任务数$fetchXml fetch aggregatetrue entity namecase attribute namecaseid aggregatecount aliastotal/ filter condition attributenew_batch_id operatoreq valueSCREEN-20260101/ /filter /entity /fetch $encoded [uri]::EscapeDataString($fetchXml) $response Invoke-RestMethod -Uri $crmUrl/api/data/v9.2/cases?fetchXml$encoded -Headers $headers $response.value[0].total把返回的total和 HIS 导出的名单行数对比。差异不为零时按statuscode分组查看缺失原因优先处理“重复档案”和“无有效联系方式”两类。这样能快速判断一次导入是数据源问题还是 CRM 端字段校验问题。5.2 批量导入提速临时停用插件和异步作业公共医疗经常要一次性导入几万条年度体检任务。如果环境里已有自动编号或随访计划插件数据插入速度会明显下降。常见做法是导入前把插件注册步骤设为停用导入完成后重新启用。这个操作要谨慎必须确认停用插件不会影响必填字段赋值否则会产生大量缺省数据。参数可以参考下面的表格批大小插件状态预期效果风险500 条启用稳定可追踪速度慢5000 条停用速度提升明显部分自定义逻辑丢失50000 条停用且关闭审核最快无法审计不建议我一般会保留“审核日志”开启只停用第三方插件。导入完成后再跑一条 FetchXML 聚合查询对比总数和必填字段为空的数量确认无误后恢复插件状态。5.3 给服务对象生成可排序健康编号的确定性技巧最后说一个容易被忽略的小技巧。公共医疗机构往往要求健康档案号不是 GUID而是“机构代码 出生日期 流水号”的 20 位编码。直接用数据库自增字段会有并发问题两个接入端同时插入时容易拿到相同流水号。推荐用专门的计数器实体在事务内执行“读取旧值、加 1、写回新值”而不是先查再写。编码规则可以这样定义new_nhc_id zone_code(6位) birthdate_compact(8位) sequence(4位)sequence 字段建议用字符串类型保存而不是 Int32避免出现“0001”被转成“1”之后排序错乱。如果有多台写入节点把计数器的分区键设为机构代码每个机构独立流水既能防止冲突又能让编号从视觉上就看出归属机构。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进