基于ASP.NET Core的住院日志统计系统设计与实现

基于ASP.NET Core的住院日志统计系统设计与实现

一、项目概述

住院日志管理系统是医院信息系统(HIS)的重要组成部分,主要用于记录和管理住院患者的入院、出院、转归等全流程信息。本文基于 ASP.NET Core + Layui 技术栈,详细讲解一个完整的住院日志管理系统的设计与实现。

技术栈

  • 后端框架ASP.NET Core 6.0
  • ORM框架:Dapper(轻量级高性能)
  • 前端框架:Layui 2.9.7
  • 数据库:SQL Server(主数据库)+ MySQL(字典数据)
  • Excel处理:NPOI 2.5+

二、核心功能架构

复制代码
┌─────────────────────────────────────────────────────────────┐
│                        视图层 (Layui)                        │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐    │
│  │ 条件查询  │  │ 数据表格  │  │ 日期范围  │  │ Excel导出 │    │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘    │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                     控制器层 (Controller)                   │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐    │
│  │  Index   │  │InPatient │  │  Export  │  │DownLoad  │    │
│  │          │  │  List    │  │  Excel   │  │  File    │    │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘    │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                      服务层 (Dapper)                         │
│  ┌──────────────────┐  ┌──────────────────┐                │
│  │  IMssqlService   │  │  IMysqlService   │                │
│  │  (SQL Server)    │  │  (MySQL)         │                │
│  └──────────────────┘  └──────────────────┘                │
└─────────────────────────────────────────────────────────────┘

三、控制器核心实现

3.1 依赖注入与构造函数

csharp 复制代码
public class InPatientLogerController : Controller
{
    private ILogger<InPatientLogerController> _logger;
    private string _sihis;          // HIS数据库连接字符串
    private string _qasystem;        // QASystem数据库连接字符串
    private readonly IMssqlService _mssqlService;
    private readonly IMysqlService _mysqlService;

    public InPatientLogerController(
        ILogger<InPatientLogerController> logger,
        IConfiguration configuration,
        IMssqlService mssqlService,
        IMysqlService mysqlService)
    {
        _logger = logger;
        _sihis = configuration.GetConnectionString("sihis");
        _qasystem = configuration.GetConnectionString("qasystem");
        _mssqlService = mssqlService;
        _mysqlService = mysqlService;
    }
}

设计亮点

  • 通过 IConfiguration 获取多数据库连接字符串,支撑 SQL Server 和 MySQL 双数据源架构
  • 使用 ILogger 记录异常日志,便于生产环境问题排查
  • 依赖注入解耦服务层,提高代码可测试性

3.2 分页查询实现(核心重点)

csharp 复制代码
public IActionResult InPatientList(InPatientLogerSearchModel model)
{
    DataTableModel<InPatientLogerModel> dataTableModel = null;
    try 
    {
        int count = 0;
        List<InPatientLogerModel> InPatientLogers = GetInPatientLogers(model, true, out count);
        
        if (count == 0)
        {
            dataTableModel = new DataTableModel<InPatientLogerModel> { 
                code = 1, 
                count = count, 
                msg = "无数据加载", 
                data = InPatientLogers 
            };
        }
        else
        {
            dataTableModel = new DataTableModel<InPatientLogerModel> { 
                code = 0, 
                count = count, 
                msg = "", 
                data = InPatientLogers 
            };
        }
    }
    catch (Exception ex)
    {
        dataTableModel = new DataTableModel<InPatientLogerModel> { 
            code = 1, 
            count = 0, 
            msg = ex.Message, 
            data = null 
        };
        _logger.LogError(ex.Message);
    }
    return Content(JsonConvert.SerializeObject(dataTableModel));
}

3.3 动态SQL构建(重点分析)

这是本文的核心重点 ,展示如何使用 Dapper 的 DynamicParameters 构建安全、高效的动态查询:

csharp 复制代码
private List<InPatientLogerModel> GetInPatientLogers(
    InPatientLogerSearchModel model, 
    bool IsPage, 
    out int count)
{
    // 1. 计算分页偏移量
    int rn = (model.page - 1) * model.limit;
    int rownum = model.page * model.limit;

    List<string> whereList = new List<string>();
    var Pm = new DynamicParameters();

    string sql = @"SELECT * FROM v_zhuyuanrz_new";
    string csql = "select count(*) from v_zhuyuanrz_new";

    // 2. 动态构建查询条件 - 入院日期范围
    if (!string.IsNullOrEmpty(model.ruyuanrq))
    {
        string[] QueryDate = model.ruyuanrq.Split('~');
        string ksrq = QueryDate[0].Trim() + " 00:00:00";
        string jsrq = QueryDate[1].Trim() + " 23:59:59";
        whereList.Add("ruyuanrq between @ksrq1 and @jsrq1");
        Pm.Add("@ksrq1", ksrq);
        Pm.Add("@jsrq1", jsrq);
    }

    // 3. 动态构建查询条件 - 出院日期范围
    if (!string.IsNullOrEmpty(model.chuyuanrq))
    {
        string[] QueryDate = model.chuyuanrq.Split('~');
        string ksrq = QueryDate[0].Trim() + " 00:00:00";
        string jsrq = QueryDate[1].Trim() + " 23:59:59";
        whereList.Add("chuyuanrq between @ksrq2 and @jsrq2");
        Pm.Add("@ksrq2", ksrq);
        Pm.Add("@jsrq2", jsrq);
    }

    // 4. 动态构建查询条件 - 精确匹配(病历号)
    if (!string.IsNullOrEmpty(model.bingrenid))
    {
        whereList.Add("bingrenid = @bingrenid");
        Pm.Add("@bingrenid", model.bingrenid);
    }

    // 5. 动态构建查询条件 - 模糊匹配(姓名)
    if (!string.IsNullOrEmpty(model.xingming))
    {
        whereList.Add("xingming like @xingming");
        Pm.Add("@xingming", $"%{model.xingming}%");
    }

    // 6. 动态构建查询条件 - 模糊匹配(性别)
    if (!string.IsNullOrEmpty(model.xingbie))
    {
        whereList.Add("xingbie like @xingbie");
        Pm.Add("@xingbie", $"%{model.xingbie}%");
    }

    // 7. 动态构建查询条件 - 科室(支持模糊搜索)
    if (!string.IsNullOrEmpty(model.keshimc))
    {
        whereList.Add("keshimc like @keshimc");
        Pm.Add("@keshimc", $"%{model.keshimc}%");
    }

    // 8. 动态构建查询条件 - 主管医生
    if (!string.IsNullOrEmpty(model.zhuguanys))
    {
        whereList.Add("zhuguanys like @zhuguanys");
        Pm.Add("@zhuguanys", $"%{model.zhuguanys}%");
    }

    // 9. 传染病筛选
    if (model.IsInfect == "on")
    {
        whereList.Add("IsInfect is not null");
    }

    // 10. 组合WHERE子句
    string whereSql = string.Join(" AND ", whereList);
    
    if (whereList.Count > 0)
    {
        csql = csql + " where " + whereSql;
        
        if (IsPage)
        {
            // SQL Server 2012+ 分页语法:OFFSET FETCH
            string orderBy = $" ORDER BY VisitNo OFFSET {rn} ROWS FETCH NEXT {rownum} ROWS ONLY";
            sql = sql + " where " + whereSql + orderBy;
        }
        else
        {
            sql = sql + " where " + whereSql;
        }
    }

    // 11. 执行查询
    count = _mssqlService.DBFind<int>(_sihis, csql, Pm);
    List<InPatientLogerModel> results = _mssqlService.DBQuery<InPatientLogerModel>(_sihis, sql, Pm);
    
    return results;
}
动态SQL构建核心要点
要点 说明
DynamicParameters Dapper 的参数化查询对象,防止 SQL 注入
List 存储条件 利用 string.Join(" AND ", whereList) 动态拼接 WHERE 子句
模糊匹配 使用 LIKE '%value%' 配合 % 通配符
分页语法 SQL Server 2012+ 的 OFFSET...FETCH NEXT 语法
参数化查询 所有用户输入都通过参数传递,杜绝 SQL 注入风险

四、Excel 导出功能实现

4.1 NPOI 导出 Excel

csharp 复制代码
[HttpPost]
public JsonResult InPatientExportExcel(InPatientLogerSearchModel model)
{
    MsgModel MsgObj = null;
    try
    {
        int count = 0;
        // 获取全部数据(不分页)
        List<InPatientLogerModel> results = GetInPatientLogers(model, false, out count);

        // 1. 创建工作簿和Sheet
        IWorkbook workbook = new XSSFWorkbook();
        ISheet sheet = workbook.CreateSheet($"住院日志");
        
        // 2. 创建表头
        IRow rowHeader = sheet.CreateRow(0);
        rowHeader.CreateCell(0, CellType.String).SetCellValue("病人ID");
        rowHeader.CreateCell(1, CellType.String).SetCellValue("病人姓名");
        rowHeader.CreateCell(2, CellType.String).SetCellValue("性别");
        rowHeader.CreateCell(3, CellType.String).SetCellValue("年龄");
        rowHeader.CreateCell(4, CellType.String).SetCellValue("身份证号");
        rowHeader.CreateCell(5, CellType.String).SetCellValue("科室");
        rowHeader.CreateCell(6, CellType.String).SetCellValue("联系地址");
        rowHeader.CreateCell(7, CellType.String).SetCellValue("联系电话");
        rowHeader.CreateCell(8, CellType.String).SetCellValue("入院日期");
        rowHeader.CreateCell(9, CellType.String).SetCellValue("主管医生");
        rowHeader.CreateCell(10, CellType.String).SetCellValue("入院诊断");
        rowHeader.CreateCell(11, CellType.String).SetCellValue("出院日期");
        rowHeader.CreateCell(12, CellType.String).SetCellValue("出院诊断");
        rowHeader.CreateCell(13, CellType.String).SetCellValue("传染病诊断日期");
        rowHeader.CreateCell(14, CellType.String).SetCellValue("传染病诊断");
        rowHeader.CreateCell(15, CellType.String).SetCellValue("转归情况");

        // 3. 写入数据行
        for (int i = 0; i < results.Count; i++)
        {
            InPatientLogerModel result = results[i];
            IRow srow = sheet.CreateRow(i + 1);
            srow.CreateCell(0, CellType.String).SetCellValue(result.bingrenid);
            srow.CreateCell(1, CellType.String).SetCellValue(result.xingming);
            srow.CreateCell(2, CellType.String).SetCellValue(result.xingbie);
            srow.CreateCell(3, CellType.String).SetCellValue(result.nianling);
            srow.CreateCell(4, CellType.String).SetCellValue(result.shenfenzh);
            srow.CreateCell(5, CellType.String).SetCellValue(result.keshimc);
            srow.CreateCell(6, CellType.String).SetCellValue(result.lianxidz);
            srow.CreateCell(7, CellType.String).SetCellValue(result.lianxidh);
            srow.CreateCell(8, CellType.String).SetCellValue(result.ruyuanrq.ToString("yyyy-MM-dd HH:mm:ss"));
            srow.CreateCell(9, CellType.String).SetCellValue(result.zhuguanys);
            srow.CreateCell(10, CellType.String).SetCellValue(result.ruyuanzd);
            srow.CreateCell(11, CellType.String).SetCellValue(result.chuyuanrq.ToString("yyyy-MM-dd HH:mm:ss"));
            srow.CreateCell(12, CellType.String).SetCellValue(result.chuyuanzd);
            srow.CreateCell(13, CellType.String).SetCellValue(result.InfectDate);
            srow.CreateCell(14, CellType.String).SetCellValue(result.IsInfect);
            srow.CreateCell(15, CellType.String).SetCellValue(result.chuyuanzg);
        }

        // 4. 生成随机文件名并保存
        string fileName = $"住院日志数据_{RandomNumber.GetVerCode()}.xlsx";
        string BasePath = Path.Combine(AppContext.BaseDirectory, "InPatientLogerExport");
        if (!Directory.Exists(BasePath))
        {
            Directory.CreateDirectory(BasePath);
        }
        string filePath = Path.Combine(BasePath, fileName);
        
        using (Stream stream = System.IO.File.OpenWrite(filePath))
        {
            workbook.Write(stream);
            MsgObj = new MsgModel { code = 0, msg = filePath };
        }
    }
    catch (Exception ex)
    {
        MsgObj = new MsgModel { code = 1, msg = ex.Message };
        _logger.LogError(ex.Message);
    }
    return Json(MsgObj);
}

4.2 文件下载实现

csharp 复制代码
[HttpGet]
public IActionResult DownLoadFile(string filePath)
{
    try
    {
        Stream stream = System.IO.File.OpenRead(filePath);
        var provider = new FileExtensionContentTypeProvider();
        var memi = provider.Mappings[".xlsx"];
        return File(stream, memi, Path.GetFileName(filePath));
    }
    catch (Exception ex)
    {
        ErrorViewModel model = new ErrorViewModel { RequestId = "500" };
        _logger.LogError(ex.Message);
        return View("~/Views/Shared/Error.cshtml", model);
    }
}

技术要点

  • 使用 FileExtensionContentTypeProvider 自动获取 MIME 类型
  • 返回 File(stream, mimeType, fileName) 实现文件下载

五、前端视图实现

5.1 Layui 表格组件

javascript 复制代码
layui.config({
    base: '/layuiadmin/'
}).extend({
    index: 'lib/index'
}).use(['index', 'useradmin', 'table', 'jquery', 'tree', 'view', 'laydate'], function () {
    var $ = layui.$
        , form = layui.form
        , table = layui.table
        , laydate = layui.laydate;

    // 渲染数据表格
    table.render({
        elem: '#PatientsTable'
        , url: '/InPatientLoger/InPatientList'
        , cols: [[
              { type: 'checkbox', fixed: 'left' }
              , { field: 'IsInfect', width: 80, align: 'center', title: '传染病', 
                  templet: '#IsInfect', fixed: 'left' }
              , { field: 'InfectDate', width: 180, align: 'center', 
                  title: '传染病诊断时间', templet: '#InfectDate' }
              , { field: 'bingrenid', width: 120, align: 'center', title: '病人ID' }
              , { field: 'xingming', width: 100, align: 'center', title: '病人姓名'}
              , { field: 'keshimc', width: 150, align: 'center', title: '科室' }
              , { field: 'ruyuanrq', width: 180, align: 'center', 
                  title: '入院日期', templet: '#ruyuanrq'}
              , { field: 'chuyuanrq', width: 180, align: 'center', 
                  title: '出院日期', templet: '#chuyuanrq' }
        ]]
        , page: true
        , limit: 20
        , limits: [20, 100, 200, 300, 400, 500, 600, 700, 1000]
        , height: 'full-220'
    });
});

5.2 条件搜索与表格重载

javascript 复制代码
// 监听条件查询提交
form.on('submit(PatientSearch)', function (data) {
    var field = data.field;
    // 执行重载 - 带条件分页查询
    table.reload('PatientsTable', {
        where: field,
        page: { curr: 1 }
    });
});

// Excel导出按钮事件
table.on('toolbar(PatientsTable)', function (obj) {
    switch (obj.event) {
        case 'PatientExportExcel':
            var loadIndex = layer.msg("数据导出中,请稍后....", { 
                icon: 16, shade: 0.01, time: 1000 * 60 * 60 
            });
            var data = form.val('demo-val-filter');
            $.post("/InPatientLoger/InPatientExportExcel", data, function (obj) {
                if (obj.code == 0) {
                    window.top.location.href = "@Url.Action("DownLoadFile")?filePath=" 
                        + encodeURIComponent(obj.msg);
                    layer.close(loadIndex);
                    layer.msg("导出成功!", { icon: 6, shade: 0.01 });
                } else {
                    layer.msg(obj.msg);
                }
            });
            break;
    };
});

5.3 日期范围选择器

javascript 复制代码
// 入院日期范围选择器
laydate.render({
    elem: '#VisitDateSelect1'
    , range: '~'
    , max: '2025-12-05'
    , mark: {
        '2025-12-05': '换HIS'
    }
});

// 出院日期范围选择器
laydate.render({
    elem: '#VisitDateSelect2'
    , range: '~'
    , max: '2025-12-05'
});

六、数据模型设计

6.1 搜索模型

csharp 复制代码
public class InPatientLogerSearchModel
{
    public int page { get; set; }       // 当前页码
    public int limit { get; set; }     // 每页条数
    public string ruyuanrq { get; set; }   // 入院日期范围 "2024-01-01 ~ 2024-01-31"
    public string chuyuanrq { get; set; }  // 出院日期范围
    public string bingrenid { get; set; }   // 病历号
    public string xingming { get; set; }    // 病人姓名
    public string xingbie { get; set; }    // 性别
    public string nianling { get; set; }   // 年龄
    public string keshimc { get; set; }    // 科室名称
    public string lianxidz { get; set; }   // 联系地址
    public string lianxidh { get; set; }    // 联系电话
    public string zhuguanys { get; set; }  // 主管医生
    public string ruyuanzd { get; set; }   // 入院诊断
    public string chuyuanzd { get; set; }  // 出院诊断
    public string chuyuanzg { get; set; }  // 转归情况
    public string IsInfect { get; set; }   // 传染病标识
}

6.2 数据模型

csharp 复制代码
public class InPatientLogerModel
{
    public string bingrenid { get; set; }      // 病人ID
    public string xingming { get; set; }       // 病人姓名
    public string xingbie { get; set; }       // 性别
    public string nianling { get; set; }       // 年龄
    public string shenfenzh { get; set; }     // 身份证号
    public string keshimc { get; set; }       // 科室名称
    public string lianxidz { get; set; }       // 联系地址
    public string lianxidh { get; set; }       // 联系电话
    public DateTime ruyuanrq { get; set; }    // 入院日期
    public string zhuguanys { get; set; }     // 主管医生
    public string ruyuanzd { get; set; }      // 入院诊断
    public DateTime chuyuanrq { get; set; }  // 出院日期
    public string chuyuanzd { get; set; }    // 出院诊断
    public string IsInfect { get; set; }      // 传染病诊断
    public string InfectDate { get; set; }   // 传染病诊断日期
    public string chuyuanzg { get; set; }    // 转归情况
}

七、关键技术总结

7.1 动态 SQL 构建最佳实践

复制代码
┌─────────────────────────────────────────────────────────┐
│                    动态SQL构建流程                        │
├─────────────────────────────────────────────────────────┤
│  1. 创建 DynamicParameters 对象                          │
│           │                                            │
│           ▼                                            │
│  2. 根据条件是否为null,动态添加到 whereList            │
│           │                                            │
│           ▼                                            │
│  3. string.Join(" AND ", whereList) 组合条件           │
│           │                                            │
│           ▼                                            │
│  4. SQL注入防护:所有值通过参数传递                      │
└─────────────────────────────────────────────────────────┘

7.2 分页查询对比

方式 语法 适用数据库
OFFSET FETCH OFFSET {start} ROWS FETCH NEXT {pageSize} ROWS ONLY SQL Server 2012+
TOP + 子查询 SELECT TOP {pageSize} * FROM (...) WHERE rn > {start} SQL Server 2005+
LIMIT LIMIT {pageSize} OFFSET {start} MySQL

7.3 安全性考虑

  1. SQL 注入防护 :所有用户输入通过 DynamicParameters 参数化传递
  2. XSS 防护:Layui 表格默认会对输出进行 HTML 编码
  3. 文件路径安全 :下载时使用 Path.GetFileName() 防止路径遍历攻击

八、总结

本文详细介绍了基于 ASP.NET Core + Layui 技术栈的住院日志管理系统,涵盖以下核心知识点:

  1. Dapper 动态 SQL 构建 :利用 DynamicParametersList<string> 实现灵活安全的动态查询
  2. SQL Server 分页 :使用 OFFSET...FETCH NEXT 现代分页语法
  3. NPOI Excel 导出:流式写入实现大数据量导出
  4. Layui 前端交互:表格组件、条件查询、日期范围选择器的综合运用
  5. 多数据库架构:SQL Server 与 MySQL 的混合使用

该系统架构清晰、代码规范,适合作为医院信息系统开发的参考模板。

相关推荐
卜夋2 小时前
Rust学习 - 变量与类型
后端
hudson20222 小时前
work_mem: 这是一个陷阱!
后端·postgresql
Nturmoils2 小时前
实时决策时代,工业物联网需要什么样的数据库?
数据库·后端
用户8356290780512 小时前
Python 实现 Word 页眉页脚添加与自定义设置
后端·python
Rick19932 小时前
Spring Boot自动装配原理
java·spring boot·后端
神奇小汤圆2 小时前
Elasticsearch 与 JVM:生产环境调优实战指南
后端
肌肉娃子2 小时前
一次 Doris FE CPU 飙高的排障实录:从怀疑 fe.conf 到定位 MyBatis 超长批量 UPSERT
后端
腥辣甜咸2 小时前
队列?不妨试试pgmq
后端
小江的记录本2 小时前
【大语言模型】大语言模型——核心概念(预训练、SFT监督微调、RLHF/RLAIF对齐、Token、Embedding、上下文窗口)
java·人工智能·后端·python·算法·语言模型·自然语言处理