基于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 安全性考虑
- SQL 注入防护 :所有用户输入通过
DynamicParameters参数化传递 - XSS 防护:Layui 表格默认会对输出进行 HTML 编码
- 文件路径安全 :下载时使用
Path.GetFileName()防止路径遍历攻击
八、总结
本文详细介绍了基于 ASP.NET Core + Layui 技术栈的住院日志管理系统,涵盖以下核心知识点:
- Dapper 动态 SQL 构建 :利用
DynamicParameters和List<string>实现灵活安全的动态查询 - SQL Server 分页 :使用
OFFSET...FETCH NEXT现代分页语法 - NPOI Excel 导出:流式写入实现大数据量导出
- Layui 前端交互:表格组件、条件查询、日期范围选择器的综合运用
- 多数据库架构:SQL Server 与 MySQL 的混合使用
该系统架构清晰、代码规范,适合作为医院信息系统开发的参考模板。