HIS系统源代码解析:门诊闭环、Ajax接口与二次开发避坑

发布时间:2026/10/25 17:44:54
HIS系统源代码解析:门诊闭环、Ajax接口与二次开发避坑

简介:面向医疗信息化前端初学者与课程实践者的一套HIS系统界面源码与说明包,聚焦登录注册、预约挂号、个人病历和药方查询等典型模块,帮助读者理解医疗业务页面的交互组织、数据获取与样式设计,适合作为课程设计、毕业设计原型或前端综合练习的参考案例。压缩包共42个文件、约736KB,以HTML页面为主体,配合JavaScript脚本、CSS样式、JSON示例数据及界面图片素材,并附说明文档,便于按页面和功能对照阅读。已有2812人学习,说明该案例在课程设计和前端练手中具有一定参考度。读者可从中梳理jQuery操作DOM与事件、Ajax局部请求及JSON数据展示的配合方式,也能观察多页面结构、公共样式拆分和图片资源组织,进而迁移到医疗类信息管理系统页面开发中。

1. 从一张挂号小票说起:HIS 系统源代码里到底装了什么

周一早上八点半,门诊大厅的队伍已经拐了两个弯。挂号窗口敲键盘、收费窗口打单、药房窗口叫号,这几个动作背后是同一个系统在支撑,也就是 HIS(医疗信息系统)。这份 HIS 系统源代码解压之后,我看到的是一条完整门诊业务闭环:挂号、分诊、医生站开单、收费结算、药房发药、住院医嘱,前端用 JavaScript 写页面、用 Ajax 拉 JSON 接口,页面不整页刷新。

适合三类人看:想搞懂医疗业务闭环到底长什么样的前后端开发者;要给小型机构做定制、需要一个能改得动的底座的小团队;还有接手老系统、每天被“这功能谁写的”折磨的维护人员。说明文档在压缩包根目录,先读它再动代码,能省掉半天瞎猜。

2. 解压之后先看哪里:目录结构与技术栈定位

一个几十兆的源码包,最怕的就是打开之后满屏文件不知道从哪下手。我的习惯是先花十分钟只做三件事:认目录、认栈、认数据库。这三件事做完,后面所有的调试动作都有落脚点。

2.1 顶层目录:把「能跑的部分」和「能改的部分」分开

解压后先看根目录,业务站点、分层程序集、数据库脚本、说明文档基本是分开摆的,结构大致是这样:

HIS/ ├── Web/ # 站点根目录,IIS/应用服务器指向这里 │ ├── Login.aspx # 登录页,会话入口 │ ├── Register/ # 挂号、退号相关页面 │ ├── Charge/ # 收费、退费、发票 │ ├── Doctor/ # 医生站:开单、医嘱、病历 │ ├── Pharmacy/ # 药房:发药、退药、库存 │ ├── js/ # 公共脚本:jquery、common.js、各模块脚本 │ ├── css/ │ └── Web.config # 连接字符串、会话、错误页配置 ├── BLL/ # 业务逻辑层,扣号源、算金额都在这 ├── DAL/ # 数据访问层,SQL 基本都落在这一层 ├── Model/ # 实体类,字段与表一一对应 ├── Common/ # 工具类:加密、日志、JSON 序列化 ├── DB/ │ ├── his_db.sql # 建库建表脚本 │ └── init_data.sql # 字典表:科室、号别、收费项目、初始账号 └── 说明文档 # 部署步骤、默认账号、环境要求

这里有个判断技巧:Web/js/目录里的文件数量和命名方式,能大致看出这套代码的“年龄”。如果每个页面配一个独立 JS、公共逻辑靠复制粘贴,那前端就偏向传统写法,改起来要小心;如果已经抽出common.js统一封装请求,说明作者至少考虑过维护性,二次开发会舒服很多。

BLL/和DAL/分开是不错的设计,但实际源码包里经常能看到 SQL 语句直接写在页面后台代码里。这种情况不用惊讶,老项目普遍如此,改造的时候把它当成技术债记下来就行,别一上来就大重构。

2.2 技术栈反推:三个文件就能判断底子

不用通读代码,看三个文件就够:

  • Web.config(或web.config):连接字符串、Session 超时、认证方式、错误页跳转都在这里,它决定了你能不能连上数据库、掉线快不快。
  • package.json或js/jquery-*.min.js的文件名:jQuery 是 1.x 还是 3.x,直接决定你能不能用Promise、$.ajax的新写法。1.x 时代很多回调还是 callback 套 callback,改造时要补defer。
  • 项目文件(.csproj/pom.xml/*.sln):能看出目标框架版本、依赖了哪些第三方库。老包常见的是 .NET Framework 4.0 或 4.5,这意味着你在新机器上装环境时不能直接上最新运行时,得装对应版本。

提示:先确认框架版本再装环境,比装完发现跑不起来再回头找要省事得多。老项目对环境版本很挑,这一点确实不好掌控。

2.3 数据库与初始账号:不先建库,前端调什么都白搭

数据库脚本一般放在DB/下,先执行建表脚本,再执行初始化数据脚本。初始化脚本里通常包含科室字典、号别字典、收费项目价表、角色权限和默认管理员账号,这四样缺一样,登录进去也是一片空白。

连接字符串在Web.config的connectionStrings节点里,常见写法是:

<!-- Web.config:改这三处就能连上你自己的库 --> <connectionStrings> <add name="HisConn" connectionString="Server=127.0.0.1;Database=HisDb;User Id=sa;Password=你的密码;" providerName="System.Data.SqlClient" /> </connectionStrings> <system.web> <!-- 门诊窗口一坐一整天,Session 给足,不然收费收到一半掉线 --> <sessionState mode="InProc" timeout="120" /> </system.web>

参数说明:Server填实例地址,命名实例要写成127.0.0.1\SQLEXPRESS这种形式;Database必须和建库脚本里的库名一致,不一致就直接报“无法打开登录所请求的数据库”;timeout单位是分钟,窗口业务建议不低于 60。

改完连接字符串,先用数据库客户端手动连一次,能连上再启动站点。这一步跳过的后果是:页面报 500,你在代码里找半天,其实只是密码错了。

3. 前端这条线:Ajax + JSON 如何把门诊流程串起来

这套系统的前端没什么花哨框架,核心就是 JavaScript + jQuery + Ajax + JSON 四件套。看着朴素,但它是整个系统里最容易改、也最容易改出问题的地方。把这条线摸清,你就能在不碰后端的情况下加字段、改校验、调页面。

3.1 公共请求封装:一个函数省掉一半重复代码

先找js/common.js。做得比较规范的包会有一个统一请求入口,所有模块都调它,而不是各写各的$.ajax:

// js/common.js —— 全站公用请求封装 var HIS = { post: function (url, data, cb, opts) { opts = opts || {}; $.ajax({ url: url, type: 'POST', dataType: 'json', // 强制按 JSON 解析,解析失败直接进 error contentType: 'application/json; charset=utf-8', data: JSON.stringify(data), // 统一序列化,后端按流读取 timeout: opts.timeout || 15000, // 收费、发药这类操作别设太短 beforeSend: function () { if (opts.mask !== false) { $('#loading').show(); } }, success: function (res) { if (res.code === 0) { cb && cb(res.data); // 只把业务数据交给回调 } else if (res.code === 401) { top.location.href = '/Login.aspx'; // 会话过期统一跳登录 } else { alert(res.msg || '操作失败'); } }, error: function (xhr, status) { // 这里最容易翻车:返回的可能是一整页登录 HTML,不是 JSON console.error('接口异常', url, status, xhr.status); alert('网络或服务异常,请重试'); }, complete: function () { if (opts.mask !== false) { $('#loading').hide(); } } }); } };

参数说明:url是接口相对路径,一般以/api/开头;data是纯业务对象,不要在里塞 DOM 节点;cb只在code === 0时被调用,业务失败不会污染回调;opts.mask传false可以关掉遮罩,比如轮询类请求就不该弹遮罩;opts.timeout在药房发药、批量结算这类操作上可以调到 30 秒。

逻辑说明:把dataType写死成json是有意为之。有些老代码用dataType: 'text'再手动eval,一旦后端返回 HTML 错误页,eval会把整段 HTML 当表达式解析,报出的错完全看不出真实原因。固定成json,解析失败会走error分支,日志里能直接看到状态码。

3.2 挂号页的一次完整交互:从点击到表格渲染

挂号页的动作很典型:填几个字段、点确认、拿到单号和排队号、右侧列表局部刷新。这段代码可以直接照抄改:

// Register/register.js —— 点击"确认挂号" $('#btnSave').on('click', function () { var form = { patientId: $('#txtPatientId').val(), // 患者主索引,刷卡或手输 deptCode: $('#selDept').val(), // 科室编码,来自字典表 doctorCode: $('#selDoctor').val(), // 医生工号,可为空表示不指定 regType: $('#selRegType').val(), // 号别:普通/专家/急诊 payType: $('#selPayType').val(), // 支付方式 visitDate: $('#txtVisitDate').val() // 就诊日期 yyyy-MM-dd }; // 前端只做必填校验,号源有没有、收多少钱,一律由后端判定 if (!form.patientId || !form.deptCode) { alert('请先选择患者和科室'); return; } HIS.post('/api/register/apply', form, function (data) { // data 结构:{ regNo, queueNo, amount, deptName } $('#txtRegNo').text(data.regNo); $('#txtQueueNo').text(data.queueNo); $('#txtAmount').text(data.amount.toFixed(2)); loadRegList(); // 局部刷新列表,不整页回发 }); });

这里有两个要点。第一,前端校验只做“必填”和“格式”,不做业务判定,因为号源余量和金额都是会变的,前端算出来的结果永远可能是旧值。第二,loadRegList()是局部刷新,只在同一个页面里重发一次查询请求然后重绘表格。老项目里经常能看到location.reload(),那就是整页刷新,体验差而且会丢表单状态。

3.3 收费与退费:金额为什么必须回后端重算

收费模块是整套系统里最不能省事的地方。我见过有人为了“体验好”,在前端把明细金额累加出来直接显示总额,然后提交给后端。这种做法一旦上线,改价就是一行浏览器控制台的事。

// Charge/charge.js —— 结算只传单号和支付方式 HIS.post('/api/charge/settle', { regNo: $('#txtRegNo').val(), payType: $('#selPayType').val(), paid: parseFloat($('#txtPaid').val()) // 患者实付,仅用于算找零 // 注意:故意不传 amount,总额以后端为准 }, function (res) { // res: { total, paid, change, items: [{ name, qty, price, amount }] } renderItems(res.items); $('#txtTotal').text(res.total.toFixed(2)); $('#txtChange').text(res.change.toFixed(2)); // 找零也由后端算 });

接口返回的 JSON 约定长这样,改动的时候保持结构不变,前端就不用跟着动:

{ "code": 0, "msg": "ok", "data": { "regNo": "MZ20240513001", "total": 128.50, "paid": 200.00, "change": 71.50, "items": [ { "name": "挂号费", "qty": 1, "price": 8.00, "amount": 8.00 }, { "name": "血常规", "qty": 1, "price": 25.00, "amount": 25.00 } ] } }

退费比收费更麻烦。合格的做法是生成一张红冲单据,原单标记为已退,不是把原记录删掉或者改成负数金额。红冲的好处是账能对上、审计能查、报表不会因为一条记录消失而偏差。

4. 后端接口与落库:一次挂号请求的数据旅程

前端点一下按钮,后端要跑完校验、扣号源、写单、算钱、写日志一整套动作。这一段是整套系统里最值得细看的地方,也是二次开发时改动风险最高的地方。

4.1 接口分层:Handler → Service → DAO 这条链路

不同版本的后端形态不太一样,常见的是通用处理程序(Handler)接请求、Service 做业务、DAO 走 SQL;Java 版本对应的是 Controller + Service + Mapper,调用关系一一对应。以 Handler 为例:

// Handler/RegisterHandler.cs —— 挂号接口入口 public class RegisterHandler : IHttpHandler { public void ProcessRequest(HttpContext ctx) { ctx.Response.ContentType = "application/json"; var body = new StreamReader(ctx.Request.InputStream).ReadToEnd(); var param = JsonConvert.DeserializeObject<RegParam>(body); Result r; try { var svc = new RegisterService(); // 操作员从会话里取,绝不信任前端传上来的工号 r = svc.Apply(param, ctx.Session["UserCode"] as string); } catch (BizException ex) { r = Result.Fail(ex.Message); } // 业务异常,提示可读 catch (Exception ex) { Log.Error(ex); // 系统异常,落日志 r = Result.Fail("系统繁忙,请重试"); // 不回显堆栈 } ctx.Response.Write(JsonConvert.SerializeObject(r)); } public bool IsReusable { get { return false; } } }

参数说明:RegParam是前端 JSON 反序列化出来的对象,字段名要和前端一致,大小写敏感;ctx.Session["UserCode"]是登录时写入的操作员工号,用于记录操作痕迹和权限判定;BizException是自定义业务异常,和系统异常分开捕获,前者提示给用户,后者只进日志。

逻辑说明:把异常分两类是关键。业务异常(号挂满了、患者不存在)要让用户看到原因;系统异常(数据库超时、空引用)不能让用户看到堆栈,否则等于把内部结构暴露出去。很多老代码把两者混在一起catch完直接返回ex.Message,上线后前端弹出一串英文堆栈,既不专业也不安全。

4.2 三张核心表:挂号、收费、医嘱怎么连

业务主键用字符串单号,不用自增 ID,这是医疗系统的常见做法,因为单号要打印、要口头报、要跨系统传。核心三张表大致是这样:

-- 挂号主表:一次挂号一行,退号用 Status 标记,不做物理删除 CREATE TABLE HIS_REGISTER ( RegNo VARCHAR(20) NOT NULL PRIMARY KEY, -- 门诊号,业务主键 PatientId VARCHAR(20) NOT NULL, -- 患者主索引 DeptCode VARCHAR(10) NOT NULL, DoctorCode VARCHAR(10) NULL, RegType VARCHAR(10) NOT NULL, -- 普通/专家/急诊 VisitDate DATE NOT NULL, Amount DECIMAL(10,2) NOT NULL DEFAULT 0, -- 挂号费,后端算 Status TINYINT NOT NULL DEFAULT 0, -- 0正常 1已退 2已就诊 OperatorCode VARCHAR(20) NOT NULL, CreateTime DATETIME NOT NULL DEFAULT GETDATE() ); CREATE INDEX IX_REG_PATIENT ON HIS_REGISTER(PatientId, VisitDate); -- 收费明细:一张收费单多行,RedFlag 标记红冲 CREATE TABLE HIS_CHARGE_ITEM ( Id BIGINT IDENTITY PRIMARY KEY, ChargeNo VARCHAR(20) NOT NULL, -- 收费单号 RegNo VARCHAR(20) NOT NULL, -- 关联挂号单 ItemCode VARCHAR(20) NOT NULL, -- 收费项目编码 Qty DECIMAL(8,2) NOT NULL DEFAULT 1, Price DECIMAL(10,2) NOT NULL, -- 下单时快照价,不随价表变 Amount DECIMAL(10,2) NOT NULL, RedFlag TINYINT NOT NULL DEFAULT 0, -- 1 表示红冲行 CreateTime DATETIME NOT NULL DEFAULT GETDATE() ); -- 医嘱表:医生站开出来的原始指令,收费从这来 CREATE TABLE HIS_ORDER ( OrderNo VARCHAR(20) NOT NULL PRIMARY KEY, RegNo VARCHAR(20) NOT NULL, ItemCode VARCHAR(20) NOT NULL, Qty DECIMAL(8,2) NOT NULL, Status TINYINT NOT NULL DEFAULT 0, -- 0新开 1已收费 2已执行 3已作废 DoctorCode VARCHAR(20) NOT NULL, CreateTime DATETIME NOT NULL DEFAULT GETDATE() );

有个细节值得注意:HIS_CHARGE_ITEM.Price存的是下单那一刻的价格快照,不是去价表里实时查。价表调整是常态,如果收费单实时关联价表,历史单据的金额会跟着变,对账永远对不平。这个坑我在别的项目里踩过,改回去要重刷数据。

4.3 事务与并发:号和钱不能出错的三个位置

门诊早高峰的并发量不小,同一时间十几个人抢同一个专家的最后几个号是常事。扣号源必须在一个带条件的更新里完成,不能先查再改:

// BLL/RegisterService.cs —— 扣号源和写单必须同事务 public Result Apply(RegParam p, string operatorCode) { using (var tran = db.BeginTransaction()) { // 1) 带条件的原子更新,影响行数为 0 说明号已被抢完 int rows = db.Execute( "UPDATE HIS_REGSOURCE SET Used = Used + 1, Version = Version + 1 " + "WHERE DeptCode = @d AND RegType = @t AND RegDate = @dt AND Used < Total", new { d = p.DeptCode, t = p.RegType, dt = p.VisitDate }); if (rows == 0) { tran.Rollback(); return Result.Fail("该号别已挂满"); } // 2) 生成门诊号并写主表 string regNo = Sequence.Next("MZ"); db.Execute( "INSERT INTO HIS_REGISTER (RegNo,PatientId,DeptCode,DoctorCode,RegType," + "VisitDate,Amount,Status,OperatorCode) " + "VALUES (@regNo,@pid,@dept,@doc,@type,@dt,@amt,0,@op)", new { regNo, pid = p.PatientId, dept = p.DeptCode, doc = p.DoctorCode, type = p.RegType, dt = p.VisitDate, amt = 8.00m, op = operatorCode }); tran.Commit(); return Result.Ok(new { regNo = regNo, amount = 8.00m }); } }

逻辑说明:WHERE Used < Total这个条件写在UPDATE里,交给数据库做原子判断,比“先SELECT余量、再UPDATE”安全得多。后者在两人同时操作时会双双通过校验,最后超发。第二个隐患是单号生成,Sequence.Next("MZ")必须是线程安全且不依赖数据库自增的,事务回滚自增不回滚,单号会出现空洞,对账时容易被误认为丢单。第三个隐患是号源表和挂号表必须同事务,只扣号不写单会白占号,只写单不扣号会超发。

5. 部署与联调避坑:五个真会卡住你的地方

这套代码我前后部署过几次,卡住的地方翻来覆去就那几个。下面按“现象 → 原因 → 解决”记下来,遇到时直接对号入座。

5.1 启动阶段:站点起不来的两个典型症状

第一条:访问首页直接 500.19,提示配置节无法识别。

原因通常是Web.config引用了当前服务器没装的模块,比如 URL 重写、自定义 HTTP 模块,或者是配置节被锁定。解决方式是先把Web.config里对应节点注释掉,把站点点起来,再去装缺失的模块回来恢复。定位的时候不要靠猜,打开 IIS 的详细错误页,错误码后面会直接写出是哪个配置节、哪一行。

第二条:首页能开,一点查询就 404,路径里还多了一段目录名。

原因是接口地址在 JS 里写成了/api/xxx这种从根起步的绝对路径,而你把站点部署在了虚拟目录下(比如/his),请求就飞到了根站点。解决办法有两种:一是在common.js里加一个baseUrl变量,由页面统一注入;二是部署时干脆建成独立站点,不挂虚拟目录。我一般选第一种,因为改一处就够,而且换部署方式时不用再动。

5.2 联调阶段:三个最容易被误判的问题

第三条:Ajax 一直进 error 分支,控制台看到返回的是一整页 HTML。

现象看着像“接口不存在”,实际是会话过期被认证模块重定向到了Login.aspx,返回状态码 200、内容类型text/html,而前端dataType: 'json'解析失败。解决分两步:后端对接口请求统一返回401加 JSON 体,而不是 302 重定向;前端在error回调里判断状态码,401 直接跳登录页。两边都做,才算稳。

第四条:写进去的中文变成问号,或者页面显示的是一串乱码。

原因是编码不统一。老项目页面头部可能是gb2312,而接口返回头是utf-8,或者数据库字段排序规则是Chinese_PRC_CI_AS、连接字符串没指定编码。解决顺序是:先统一数据库字段排序规则,再把页面、接口响应头、连接字符串三处的编码统一到utf-8,最后清一次浏览器缓存重新验证。只改一处往往只是把乱码换了个位置出现。

第五条:金额显示成 128.49999999,或者明细累加和总额差一分钱。

前端用浮点数做加法就会出现这个。解决方式是后端一律返回两位小数的字符串或以“分”为单位的整数,前端只做格式化和展示,不做累加运算。明细的总额、找零、折扣全部后端算好再下发。这条看着琐碎,但退费、对账、发票打印全依赖它,改晚了就是一片返工。

注意:遇到怪问题先看返回内容类型和状态码,再看业务代码。多数“玄学”问题在第一步就能定位。

6. 二次开发:把通用 HIS 改成贴合自己流程的四个着力点

这套代码拿到手,大多数人最终都会改。改的位置其实很集中,找准了改动量不大。

一是字典和价表。科室、号别、收费项目都在初始化脚本的字典表里,新增一个项目不用改代码,插数据就行,但Price字段要理解清楚它是快照源头,别直接改历史单据的价格。二是打印模板。挂号条、发票、医嘱单基本都是独立模板文件,改版式不动业务逻辑。三是报表。日常报表多半是 SQL 拼出来的,加一张统计表就是加一段查询加一个页面。四是对外接口。检验检查、结算对接这类,建议单独加一个 JSON 网关层,不要让外部系统直接打到业务 Handler 上,否则后面每次对接都要动核心代码。

验证方法我习惯用一条链路走通:挂号 → 开单 → 收费 → 发药 → 退费 → 红冲,每一步都查数据库里的状态字段和金额,全部对上才算改完。中间任何一步状态不对,先停,别往下走。

早些年我图快,改完收费逻辑只测了收费本身,退费没测,第二天窗口退号直接金额对不上,被叫回去加班重刷了一遍数据。从那以后我每次动账相关的代码,都强制把“收费—退费—红冲—再对账”这条链路完整走一遍,哪怕改动只有一行。希望帮到你。

本文还有配套的精品资源,点击获取