资讯动态

C# 打造医保接口自动传输 Windows 服务:从轮询到部署

发布时间:2026/10/26 23:50:23 来源:尧图企业网站定制
简介基于C#的Windows服务版医保接口自动传输程序面向医疗信息化开发者、系统集成商及需要对接医保系统的实施工程师。程序以Windows服务方式在后台运行自动完成医疗机构与医保系统间的数据交换覆盖患者信息、诊疗项目、药品费用等医保业务数据的编码、提交与回执处理可显著减少人工干预提升数据传输的及时性与稳定性。压缩包共41个文件以C#源文件(.cs)、项目配置(.config/.xml)、可执行程序(.exe)和批处理脚本(.bat)为主辅以解决方案与资源文件整体包体仅61KB方便快速部署与代码研读。资源内置完整的服务启动、停止与安装机制并包含数据库访问与错误日志模块适合作为C# Windows服务开发或医保接口对接的参考原型。目前已有217人学习下载对于需要了解医保接口实现细节或复用后台传输框架的开发者而言这份代码提供了可直接查看和修改的完整工程结构能帮助理解Windows服务生命周期、接口通信与异常处理等关键环节。1. 医保接口自动传输程序winservic C# 版一个替你跑腿的 Windows 服务医保接口自动传输程序最典型的落地场景是医院信息科每天凌晨被电话叫醒医保平台那边报数据没上去白天的门诊记录、药品费用、诊疗项目差了一大截得赶紧手工补传。这份 winservic C# 版源码解决的正是这件事——把人工导出、整理、上传的一串动作变成后台 Windows 服务自动轮询、自动推送跑到半夜也没人守着。它适合两类人正在做 HIS 与医保系统对接的实施工程师想抄现成服务骨架的 C# 开发。全文围绕 Service1.cs 服务主类、SqlDB.cs 数据库封装、ErrorLogTxt.cs 日志模块和 ProjectInstaller 安装器四个核心文件展开看完能直接照着重构一个能用的传输服务。2. 先拆骨架AutoService 的工程结构与三个核心类拿到压缩包先别急着编译源码里值得看的文件就这么几个AutoService.csproj 是主工程文件Service1.cs 是服务主类继承 ServiceBaseSqlDB.cs 封装了所有数据库访问ErrorLogTxt.cs 负责写文本日志ProjectInstaller.cs 是安装器App.config 管全部运行参数。obj 和 bin 目录是编译产物UpgradeLog 系列是 Visual Studio 升级项目时自动生成的报告没有任何运行价值直接忽略。很多 C# 开发第一次接触 Windows 服务会误以为它和 WinForms 程序一样有个界面入口。实际上服务程序的 Main 函数里跑的是 ServiceBase.Run由服务控制管理器SCM负责拉起和停止没有窗体、没有控制台。这个项目把 ServiceBase 子类单独拆在 Service1.cs 里数据库和日志分别封装成两个静态类目的是让 OnStart 和 OnStop 的生命周期代码保持干净——这是 Windows 服务项目最常见的分层习惯。2.1 Service1.cs服务主类与 OnStart/OnStop 到底干了什么服务主类是整个程序的入口所有业务逻辑都挂在 OnStart 里启动、OnStop 里释放。最常见的写法是用 System.Timers.Timer 做轮询而不是在 OnStart 里直接跑一个死循环——后者会让服务控制管理器认为启动超时直接判定启动失败。源码包的骨架逻辑我按常见做法还原如下using System.ServiceProcess; using System.Timers; namespace AutoService { public partial class Service1 : ServiceBase { private Timer _timer; public Service1() { InitializeComponent(); // 服务注册名安装后显示在服务管理器里 ServiceName AutoService; } protected override void OnStart(string[] args) { // 从 App.config 读取轮询间隔毫秒取不到就默认 60 秒 double interval 60000; string cfg System.Configuration.ConfigurationManager.AppSettings[PollIntervalMs]; if (!double.TryParse(cfg, out interval) || interval 1000) { interval 60000; } _timer new Timer(interval); _timer.Elapsed OnTimerElapsed; _timer.Start(); ErrorLogTxt.Write(服务已启动轮询间隔: interval ms); } protected override void OnStop() { if (_timer ! null) { _timer.Stop(); _timer.Dispose(); _timer null; } ErrorLogTxt.Write(服务已停止); } private void OnTimerElapsed(object sender, ElapsedEventArgs e) { // 先停掉计时器防止上一轮还没跑完又触发新一轮 _timer.Stop(); try { SqlDB.TransferPendingData(); } catch (System.Exception ex) { ErrorLogTxt.Write(传输异常: ex.ToString()); } finally { _timer.Start(); } } } }这段代码里最关键的是_timer.Stop(); ... finally { _timer.Start(); }的写法。医保数据量大时一次传输可能超过轮询间隔不先停掉 Timer 就会多个线程同时处理同一批数据轻则重复上传重则数据库锁死。把耗时操作包在 try/catch 里还有一个好处Elapsed 事件回调里抛出的异常不会让服务崩溃而是落到日志里。ServiceName属性要和安装器里的 ServiceInstaller.ServiceName 保持一致否则装上后服务管理器里会看到一个名字、服务注册表里存着另一个名字卸载时容易出问题。轮询间隔我一般会设成 3060 秒医保接口对频率没有硬性要求但推得太勤会给 HIS 数据库带来额外压力。2.2 SqlDB.cs数据库访问封装的边界SqlDB.cs 在源码包里是个静态类职责很纯粹——把连接管理和 SQL 执行全部收口。我见过不少项目把连接串散写在各个方法里出了生产事故排查时满世界找代码这个项目的做法值得抄using System.Data; using System.Data.SqlClient; namespace AutoService { public static class SqlDB { // 连接串统一从 App.config 取禁止散落各方法 private static string ConnStr { get; } System.Configuration.ConfigurationManager.ConnectionStrings[MssqlConn].ConnectionString; public static DataTable Query(string sql) { using (var conn new SqlConnection(ConnStr)) using (var cmd new SqlCommand(sql, conn)) { conn.Open(); var dt new DataTable(); using (var da new SqlDataAdapter(cmd)) { da.Fill(dt); } return dt; } } public static int Execute(string sql) { using (var conn new SqlConnection(ConnStr)) using (var cmd new SqlCommand(sql, conn)) { conn.Open(); return cmd.ExecuteNonQuery(); } } } }using块保证了连接用完即释放这是最基础的资源管理不能省。Query 方法返回 DataTable适合把待传输数据一次性拉到内存Execute 方法返回受影响行数适合执行更新状态标记的 SQL。真正要注意的是连接串建议设Connect Timeout30医保接口高峰期数据库负载高默认 15 秒可能不够。另一个细节是 SqlDB 里不应写具体业务 SQL业务组装放到单独的方法里比如 TransferPendingData否则数据库类的职责就变味了。如果后续要改成 Oracle 或 MySQL只需要替换这个类内部实现Service1.cs 完全不用动。2.3 ErrorLogTxt.cs日志写得细排查才省力医疗服务跑在没人看的服务器上日志就是唯一的黑匣子。ErrorLogTxt.cs 的实现思路是按天落文件每条记录带时间戳写入失败时不能影响主流程using System.IO; namespace AutoService { public static class ErrorLogTxt { private static readonly string LogDir Path.Combine(AppDomain.CurrentDomain.BaseDirectory, logs); public static void Write(string msg) { try { if (!Directory.Exists(LogDir)) { Directory.CreateDirectory(LogDir); } // 按天生成日志文件避免单文件无限膨胀 string file Path.Combine(LogDir, DateTime.Now.ToString(yyyyMMdd) .log); File.AppendAllText(file, ${DateTime.Now:yyyy-MM-dd HH:mm:ss} {msg}{Environment.NewLine}); } catch { // 日志写入失败不能再抛否则会拖垮传输主流程 } } } }按天切文件这个习惯强烈建议保留单文件日志跑到几个月后会有几百 MB远程排查时用记事本根本打不开。日志路径用 BaseDirectory 而不是相对路径是因为 Windows 服务的工作目录经常不在程序目录下写成logs可能把日志丢到 System32 里。源码包里这份日志类只提供了最基础的写操作实际生产我会再加两个重载一个只写字符串另一个接受 Exception 并把堆栈完整落盘。堆栈信息在排查时比什么都管用。2.4 App.config接口地址、连接串与轮询间隔的配置约定配置文件是服务的“总闸”对应源码包里的 App.config。常见参数如下配置项位置作用建议值MssqlConnconnectionStringsHIS 数据库连接串按实际环境PollIntervalMsappSettings轮询间隔毫秒60000ApiUrlappSettings医保接口推送地址按医保平台文档MaxRetryCountappSettings单条数据最大重试次数3?xml version1.0 encodingutf-8? configuration connectionStrings add nameMssqlConn connectionStringServer127.0.0.1;DatabaseHisDb;User Idsa;Passwordyourpass;Connect Timeout30; providerNameSystem.Data.SqlClient / /connectionStrings appSettings add keyPollIntervalMs value60000 / add keyApiUrl valuehttp://his-gateway:8080/medical-insurance/push / add keyMaxRetryCount value3 / /appSettings /configuration注意 connectionStrings 里如果有密码装到生产环境时建议改用 Windows 身份验证或者在安装后立刻修改配置文件权限只允许服务账户和 administrators 读取。ApiUrl 这个地址医保平台会给正式文档测试阶段一般指到测试网关上线前一定要确认切换。配置文件的坑大多出在“改完忘重启”。Windows 服务不像普通程序每次双击都重新读配置App.config 只在进程启动时加载一次改了 PollIntervalMs 不重启服务等于没改。排查配置问题时先看服务启动时间是不是早于文件修改时间。3. 把服务装进系统ProjectInstaller 与 installutil 完整安装流程源码跑不起来写得再漂亮也是白搭。Windows 服务的安装不像普通软件有 Setup.exe它靠的是 ProjectInstaller.cs 里声明的安装参数配合 .NET 自带的 InstallUtil.exe 完成注册。整个过程熟练的话五分钟内能搞定但每一步都有讲究。3.1 ProjectInstaller.cs 里设置了哪些安装参数ProjectInstaller.cs 在源码包里属于“一看就懂、一写就错”的文件。它继承 Installer构造时挂两个组件ServiceProcessInstaller 控制服务账户ServiceInstaller 控制服务名称和启动方式using System.ComponentModel; using System.Configuration.Install; using System.ServiceProcess; namespace AutoService { [RunInstaller(true)] public partial class ProjectInstaller : Installer { private ServiceProcessInstaller processInstaller; private ServiceInstaller serviceInstaller; public ProjectInstaller() { InitializeComponent(); processInstaller new ServiceProcessInstaller(); // 使用本地系统账户避免域账户密码过期导致服务启动失败 processInstaller.Account ServiceAccount.LocalSystem; serviceInstaller new ServiceInstaller(); serviceInstaller.ServiceName AutoService; serviceInstaller.DisplayName 医保接口自动传输程序; serviceInstaller.Description 定时从 HIS 提取数据并推送至医保平台; // 开机自启断电重启后不用人工拉起 serviceInstaller.StartType ServiceStartMode.Automatic; Installers.AddRange(new Installer[] { processInstaller, serviceInstaller }); } } }ServiceAccount.LocalSystem是最省心的选择权限极大能访问本地文件、注册表和绝大多数数据库连接。代价是安全风险一旦程序被入侵攻击者获得的是系统级权限。如果医院安全规范严格可以改成 NetworkService但要保证该账户对日志目录有写入权限否则服务启动后日志写不进去问题难排查。ServiceName 必须和 Service1.cs 里设置的一致这是服务注册表的主键。3.2 从源码到服务编译、安装、启动三步命令先把代码编译成可执行程序。用 Visual Studio 打开 AutoService.sln直接 Build 生成 Debug 或 Release 版本得到 AutoService.exe严格说服务程序的 exe 不能双击跑双击会弹一个错误提示这是正常的。然后以管理员身份打开命令提示符或 PowerShell执行安装# 进入 .NET Framework 工具目录版本号以本机为准 cd C:\Windows\Microsoft.NET\Framework64\v4.0.30319 # 安装服务路径换成你实际的 exe 所在目录 InstallUtil.exe D:\MedicalInsurance\AutoService\bin\Release\AutoService.exe看到 “Commit” 完成提示后服务已经注册到系统里。接着启动服务有两条路服务管理器图形界面或命令 sc# 启动服务 sc start AutoService # 查看服务状态 sc query AutoService卸载时不能直接删 exe要先停服务再执行 /u 参数sc stop AutoService InstallUtil.exe /u D:\MedicalInsurance\AutoService\bin\Release\AutoService.exe我遇到过最典型的翻车操作是直接删除 exe 文件然后在服务管理器里发现服务还在列表里启动就报“找不到文件”。服务注册信息写在注册表里不经过 InstallUtil /u 或 sc delete是清不干净的。安装失败的常见原因是权限不足。InstallUtil 必须是管理员角色否则会报“权限不足无法访问某些系统资源”。另外 64 位系统上有 Framework 和 Framework64 两个目录装了 64 位 exe 就用 64 位目录下的 InstallUtil混用可能导致服务能装但启动时报错。3.3 装完之后怎么确认服务真的在传数据服务启动后从“已安装”到“正常工作”还差一步验证。我最常做的检查有三个按顺序来服务状态确认。执行sc query AutoServiceSTATE 列应为 RUNNING。如果显示 STOPPED立刻看事件查看器Windows 日志 → 应用程序OnStart 异常通常写在这里。看日志文件是否滚动。打开程序目录下的 logs 文件夹正常情况下 “服务已启动” 这行日志已经写入。等一个轮询周期后日志里如果出现“传输异常”说明程序活了但业务没跑通问题多半出在数据库连接或接口地址上。查数据库状态标记。待传输表里的数据如果被标记为“已发送”说明完整链路已经打通。这一步在第 5 章展开这里先记住服务状态和业务状态是两回事服务 RUNNING 不代表数据一定传上去了。# 一条命令同时看服务和日志目录适合写进部署文档 sc query AutoService dir D:\MedicalInsurance\AutoService\logs命令里的 是 PowerShell 的语法cmd 里同样支持。配合日志文件的时间戳看基本能判断服务是否在按预期工作。4. 医保传输服务的五个常见坑现象、原因与处理这类服务在实施现场踩过的坑比代码本身更值钱。以下五条都是我拆项目时反复见到的真实故障按“现象 → 原因 → 解决”写方便直接对着排查。4.1 服务一启动就自动停止事件查看器里报异常现象安装一切正常但 sc start 后服务立即停止或启动时提示“服务没有及时响应启动或控制请求”。事件查看器里能看到 Service Control Manager 报错。原因OnStart 方法里抛了未捕获异常或者 OnStart 里跑了耗时过长的操作比如直接做了数据库连接和全量查询超过了 SCM 默认的 30 秒启动超时。服务启动超时后会被系统强制杀掉。解决OnStart 里只启动 Timer所有耗时操作丢到 Elapsed 回调里异步执行并且用 try/catch 包住。按第 2 章的代码骨架写就不会踩这个坑。SQL 连接测试放到服务的 onstart 阶段就属于超时高危区一定不要干。4.2 服务显示正在运行但数据一条都没传上去现象服务管理器里状态是“正在运行”事件日志也没有异常但医保平台那边就是收不到数据数据库里的待传输记录一直没变化。原因Timer 的回调里异常被吞掉了日志写入也静默失败。最常见的是 ErrorLogTxt 的日志目录没有写入权限异常信息根本落不了盘等于整个黑匣子都黑了。另一种可能是轮询间隔被改得极大比如误设成 8640000024 小时看起来在运行实际上一天才触发一次。解决先检查日志目录权限确保服务账户能写。再打开 App.config 确认 PollIntervalMs 数值手工改成 10000 后重启服务10 秒内看日志是否有输出。这个“缩小间隔做测试”的方式是我每次排查定时类服务问题的第一步。4.3 传上去的数据中文乱码医保平台显示一堆问号现象接口调用成功但医保平台回显的患者姓名、诊断名称全是乱码。原因程序侧用 UTF-8 编码发送报文医保平台要求 GB2312/GBK或者反过来。这是医保接口对接里最常见的编码错位。很多医保接口的报文是 XML 或 JSONHTTP 头里声明的 charset 和实际编码不一致时平台按声明值解码乱码就出来了。解决在与接口通信的代码里显式指定编码不要依赖系统默认。发送前把字符串用指定编码转成字节数组并在 HTTP 头里声明同样的 charset。如果报文是 XML 文件传输文件头?xml version1.0 encodingGB2312?的声明也要一并核对。4.4 数据库连接串配错服务在测试环境正常、生产环境连不上现象本地调试一切正常部署到生产服务器后日志报“在建立与服务器的连接时出错”或者 “Login failed for user”。原因连接串里写的是Server127.0.0.1指向了生产服务器本机而不是数据库实例或者 SQL 账户密码在生产环境被改了。另外很多服务器装的是命名实例连接串要写成Server服务器名\实例名不是只有 IP。解决连接串放到 App.config 后用 PowerShell 读出来核对一遍重点看 Server、端口和账号。我在部署文档里习惯加一条强制规定每台环境单独提供一份 App.config绝不使用“测试环境拷过去改个 IP”的偷懒做法——漏改一处就够排查一晚上。4.5 服务重启后同一批数据被重复推送现象医保平台出现大量重复记录患者一条缴费记录被推了四五次。原因程序从待传输表取数时没有加状态标记或者取了数据但没更新状态就落库。服务重启、定时器重入都会导致同一批数据被再次读取、再次推送。解决取数和标记状态放在同一个事务里取到数据后立刻把状态改为“传输中”推送成功后改为“已发送”失败则改为“待重试”。SQL 里用UPDATE ... WHERE Status待传输这种原子操作替代先查后改。这样即使服务在推送中途重启已标记的记录也不会再被捞起来。5. 从源码到能跑的传输任务建表、配置与定时策略落地源码包解决的是“框架”真正让传输业务跑起来还差最后一块拼图待传输数据从哪来、推到哪去、推完怎么标记。这一章补上这三个问题的标准答案。5.1 建一张待传输表程序才知道“传什么”Windows 服务本身不产生医保数据它只搬运数据。HIS 那边需要有一张中间表把要推送的记录插进去服务轮询时从这张表取数。建表脚本按最小可用标准设计CREATE TABLE dbo.MedicarePushQueue ( Id INT IDENTITY(1,1) PRIMARY KEY, BizType VARCHAR(20) NOT NULL DEFAULT VISIT, -- 业务类型就诊/药品/收费 BizContent NVARCHAR(MAX) NOT NULL, -- 推送报文JSON/XML文本 Status TINYINT NOT NULL DEFAULT 0, -- 0待传输 1传输中 2已发送 3失败 RetryCount TINYINT NOT NULL DEFAULT 0, -- 已重试次数 CreateTime DATETIME NOT NULL DEFAULT GETDATE(), -- 记录创建时间 SendTime DATETIME NULL, -- 实际发送时间 LastError NVARCHAR(500) NULL -- 最后一次失败原因 ); CREATE INDEX IX_MedicarePushQueue_Status ON dbo.MedicarePushQueue(Status, CreateTime);BizContent 字段直接存拼接好的报文这样服务取数后不需要再去详情表关联查询逻辑最薄。Status 是状态机核心0 → 1 → 2 是正常流转0 → 1 → 3 是失败流转。Index 必须要建服务每轮都按 Status 查询没有索引的话待传输数据过十万后查询会明显变慢。HIS 那边往这张表插数据的时机通常是收费完成、医嘱提交等业务节点。如果没有现成触发器也可以让 DBA 在核心业务表上建触发器自动落队列但维护成本高不建议一开始就上。5.2 定时器与轮询策略间隔设多少合理轮询间隔不是越短越好。医保平台对接口调用频率通常有约束而且凌晨的批量数据上传本身就是医保系统的低峰期。按这张表配置参考时段业务特征轮询间隔理由白天8:00–20:00实时性要求高30 秒患者刚结算完尽快推送夜间20:00–8:00数据量大、无实时压力5 分钟批量累积避免频繁请求实现方式不需要改代码在 App.config 里加两个参数比如DayIntervalMs和NightIntervalMsOnStart 时按当前时间选一个然后在 Elapsed 回调里每次触发后重新判断时段。这个方案有一个好处不需要额外引入 Quartz 之类的调度库一个 Timer 加一个时间判断就够源码包保持轻量。5.3 失败重试与幂等重复传输怎么避免接口调用不可能永远成功。医保平台偶尔返回超时、网络抖动、报文校验失败都需要重试机制。重试的常见做法是给 MaxRetryCount 设上限超过后把状态置为 3不再自动处理留待人工介入UPDATE dbo.MedicarePushQueue SET Status 3, LastError err WHERE Id id AND RetryCount 3;这里的WHERE Id id AND RetryCount 3用条件更新避免并发下重复推送。重试计数加一和状态更新的 SQL 要作为一条语句执行UPDATE dbo.MedicarePushQueue SET Status 1, RetryCount RetryCount 1 WHERE Id id AND Status IN (0, 3);幂等性的底线保障是状态标记。即使业务报文被重复推送状态机的流转也保证每次重启后不会再捞同一批数据。至于医保平台侧的去重一般在报文里带上业务唯一键比如结算流水号平台按这个键做幂等这两层缺一不可。6. 验证传输可靠性的一个习惯把每次调用都留在日志里服务部署完最怕的是没人知道它是不是还在正常工作。我养成的习惯是“手工造一条数据强制走一遍闭环”每次部署新环境、改完接口参数都做一次五分钟内就能确认整条链路是否通畅。手工造数据的操作是这样的往 MedicarePushQueue 表里插一条测试记录BizContent 填一段简单的 XML 或 JSON 报文Status 置 0然后重启服务或等下一个轮询周期。等一分钟后查这张表的状态字段和 logs 目录的日志文件SELECT Status, RetryCount, SendTime, LastError FROM dbo.MedicarePushQueue WHERE Id TestId;如果 Status 变成 2说明取数、推送、标记全链路正常如果停在 3LastError 字段会给出接口返回的具体错误如果停在 0说明服务根本没有取到这条数据问题在轮询或 SQL 层。日志文件里对应应该有一条“传输完成”记录和数据库状态字段互相印证。日志要写成结构化文本方便事后用 grep 或 PowerShell 筛。我常用的格式是[INFO] [2025-01-15 23:41:02] TransmitSuccess Id12345 Cost213ms带 Id 和耗时两个关键信息。排查慢问题时拿出同一批 Id 的多条记录对比 Cost 字段就能定位是哪一段链路变慢了。这个习惯源自一次真实教训一个传输服务在生产环境跑了三个月某天医保平台升级接口协议旧报文全部被拒但服务日志没记报文内容只写了“failed”光判断失败原因就花了整整两天。从那以后我要求每次接口调用至少记录请求报文摘要、响应码和耗时三个字段数据量再大也不能省。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑