简介:基于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 是服务主类(继承 ServiceBase),SqlDB.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 保持一致,否则装上后服务管理器里会看到一个名字、服务注册表里存着另一个名字,卸载时容易出问题。轮询间隔我一般会设成 30~60 秒,医保接口对频率没有硬性要求,但推得太勤会给 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 Timeout=30,医保接口高峰期数据库负载高,默认 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。常见参数如下:
| 配置项 | 位置 | 作用 | 建议值 |
|---|---|---|---|
| MssqlConn | connectionStrings | HIS 数据库连接串 | 按实际环境 |
| PollIntervalMs | appSettings | 轮询间隔(毫秒) | 60000 |
| ApiUrl | appSettings | 医保接口推送地址 | 按医保平台文档 |
| MaxRetryCount | appSettings | 单条数据最大重试次数 | 3 |
<?xml version="1.0" encoding="utf-8"?> <configuration> <connectionStrings> <add name="MssqlConn" connectionString="Server=127.0.0.1;Database=HisDb;User Id=sa;Password=yourpass;Connect Timeout=30;" providerName="System.Data.SqlClient" /> </connectionStrings> <appSettings> <add key="PollIntervalMs" value="60000" /> <add key="ApiUrl" value="http://his-gateway:8080/medical-insurance/push" /> <add key="MaxRetryCount" value="3" /> </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 AutoService,STATE 列应为 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 的日志目录没有写入权限,异常信息根本落不了盘,等于整个黑匣子都黑了。另一种可能是轮询间隔被改得极大,比如误设成 86400000(24 小时),看起来在运行,实际上一天才触发一次。
解决:先检查日志目录权限,确保服务账户能写。再打开 App.config 确认 PollIntervalMs 数值,手工改成 10000 后重启服务,10 秒内看日志是否有输出。这个“缩小间隔做测试”的方式是我每次排查定时类服务问题的第一步。
4.3 传上去的数据中文乱码,医保平台显示一堆问号
现象:接口调用成功,但医保平台回显的患者姓名、诊断名称全是乱码。
原因:程序侧用 UTF-8 编码发送报文,医保平台要求 GB2312/GBK,或者反过来。这是医保接口对接里最常见的编码错位。很多医保接口的报文是 XML 或 JSON,HTTP 头里声明的 charset 和实际编码不一致时,平台按声明值解码,乱码就出来了。
解决:在与接口通信的代码里显式指定编码,不要依赖系统默认。发送前把字符串用指定编码转成字节数组,并在 HTTP 头里声明同样的 charset。如果报文是 XML 文件传输,文件头<?xml version="1.0" encoding="GB2312"?>的声明也要一并核对。
4.4 数据库连接串配错,服务在测试环境正常、生产环境连不上
现象:本地调试一切正常,部署到生产服务器后日志报“在建立与服务器的连接时出错”,或者 “Login failed for user”。
原因:连接串里写的是Server=127.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和NightIntervalMs,OnStart 时按当前时间选一个,然后在 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,说明取数、推送、标记全链路正常;如果停在 3,LastError 字段会给出接口返回的具体错误;如果停在 0,说明服务根本没有取到这条数据,问题在轮询或 SQL 层。日志文件里对应应该有一条“传输完成”记录,和数据库状态字段互相印证。
日志要写成结构化文本,方便事后用 grep 或 PowerShell 筛。我常用的格式是[INFO] [2025-01-15 23:41:02] TransmitSuccess Id=12345 Cost=213ms,带 Id 和耗时两个关键信息。排查慢问题时,拿出同一批 Id 的多条记录,对比 Cost 字段就能定位是哪一段链路变慢了。这个习惯源自一次真实教训:一个传输服务在生产环境跑了三个月,某天医保平台升级接口协议,旧报文全部被拒,但服务日志没记报文内容,只写了“failed”,光判断失败原因就花了整整两天。从那以后我要求每次接口调用至少记录请求报文摘要、响应码和耗时三个字段,数据量再大也不能省。希望帮到你。
本文还有配套的精品资源,点击获取