为医院量身定制:智能定时任务服务的架构设计与实现(下)
为医院量身定制:智能定时任务服务的架构设计与实现(下)
写在前面:本文是定时任务服务系列的下篇,重点介绍定时任务的Web管理后台设计与实现。在上篇中,我们详细讲解了Windows服务的定时调度机制、反射调用、任务执行方式等核心内容。本篇将从医疗信息专家的视角,深入剖析如何构建一个功能完善、体验良好的任务管理后台。
一、引言
在医疗信息系统中,定时任务承担着数据同步、提醒通知、报表生成等关键职能。然而,随着医院业务规模的扩大和信息化程度的提升,定时任务的数量和复杂度也在不断增长。如何让运维人员高效地管理这些任务,成为系统设计中不可忽视的问题。
一个优秀的任务管理后台需要满足以下核心需求:
- 可视化配置:无需修改代码即可添加、修改、禁用任务
- 实时状态监控:及时了解任务执行情况
- 多维度查询:快速定位特定任务
- 日志追溯:任务执行过程的完整记录
- 灵活的触发策略:支持定时、间隔、条件触发等多种模式
本文将基于实际的.NET Core + Layui框架实现,详细讲解任务管理后台的架构设计与实现细节。
二、系统整体架构
2.1 技术选型
本系统采用前后端分离的现代化架构:
| 层级 | 技术栈 | 说明 |
|---|---|---|
| 前端框架 | Layui 2.9.7 | 轻量级模块化前端UI框架 |
| 后端框架 | ASP.NET Core | 跨平台高性能Web框架 |
| 数据库 | MySQL | 存储任务配置和执行日志 |
| ORM | Dapper | 轻量级ORM,支持高性能查询 |
| 前端组件 | xm-select | 多选组件,用于人员选择等场景 |
2.2 项目结构
QASystem/
├── Controllers/
│ └── TaskController.cs # 任务管理控制器
├── Models/
│ └── Task/
│ ├── TaskModel.cs # 任务数据模型
│ └── TaskLogModel.cs # 任务日志模型
└── Views/
└── Task/
├── Task.cshtml # 任务列表主页面
├── TaskAdd.cshtml # 任务新增页面
├── TaskEdit.cshtml # 任务编辑页面
├── ShowSql.cshtml # SQL脚本查看页面
└── ShowLog.cshtml # 任务日志查看页面
2.3 核心数据模型
TaskModel 任务模型
public class TaskModel
{
public int id { get; set; } // 任务ID
public string task_name { get; set; } // 任务名称(方法名)
public string task_desc { get; set; } // 任务描述(显示名称)
public string time { get; set; } // 触发时间(HH:mm格式)
public string date { get; set; } // 触发日期(01-31)
public string month { get; set; } // 触发月份(多选)
public int IntervalTime { get; set; } // 间隔时间(分钟)
public string accept_chat { get; set; } // 钉钉群ID
public string accept_emp { get; set; } // 接收人(多选,存储员工编号)
public string accept_rebot { get; set; } // 机器人Webhook地址
public string sendType { get; set; } // 推送类型(0成功/1失败)
public DateTime dtTime { get; set; } // 创建时间
public int method { get; set; } // 处理方式(0内部/1SQL/2GET/3POST)
public string handle { get; set; } // 具体处理内容
public int data_source { get; set; } // 数据源(0无/1联众HIS/2矽岛EMR...)可扩展
public int useflag { get; set; } // 启用状态(0禁用/1启用)
public int task_status { get; set; } // 任务状态(0未触发/1成功/2失败)
public DateTime? updateTime { get; set; } // 最后执行时间
}
TaskLogModel 日志模型
public class TaskLogModel
{
public int id { get; set; }
public int task_id { get; set; } // 关联任务ID
public string record_content { get; set; } // 日志内容(JSON格式)
public DateTime record_time { get; set; } // 记录时间
}
三、任务控制器设计
TaskController是整个管理后台的核心枢纽,负责处理所有任务相关的HTTP请求。我们采用分层设计,将业务逻辑与数据访问分离,通过依赖注入获取数据库服务。
3.1 控制器初始化
public class TaskController : CheckLoginController
{
private ILogger<IndexController> _logger;
private string _qasystem;
private string _harmfulevent;
private IMysqlService _mysqlService;
public TaskController(ILogger<IndexController> logger,
IConfiguration configuration,
IMysqlService mysqlService)
{
_logger = logger;
_qasystem = configuration.GetConnectionString("qasystem");
_harmfulevent = configuration.GetConnectionString("harmfulevent");
_deepthroat = configuration.GetConnectionString("deepthroat");
_mysqlService = mysqlService;
}
}
设计亮点:
- 多数据库连接支持:通过配置文件管理多个数据库连接字符串,适应医院复杂的信息系统环境
- 依赖注入:使用接口抽象数据访问层,便于单元测试和扩展
- 继承CheckLoginController:确保所有操作都需要身份验证,保障系统安全
3.2 任务列表查询
任务列表是管理后台的核心页面,需要支持分页、模糊搜索、状态筛选等功能。
[HttpGet]
public IActionResult TaskList(TaskModel model)
{
int offset = (model.page - 1) * model.limit;
int rows = model.limit;
// 构建动态查询条件
List<string> whereList = new List<string>();
var countPm = new DynamicParameters();
var pagePm = new DynamicParameters();
// 模糊搜索:支持按任务名称和描述搜索
if (!string.IsNullOrEmpty(model.task_desc))
{
whereList.Add("(task_desc like ?task_desc or task_name like ?task_name)");
countPm.Add("?task_desc", "%" + model.task_desc + "%");
pagePm.Add("?task_desc", "%" + model.task_desc + "%");
countPm.Add("?task_name", "%" + model.task_desc + "%");
pagePm.Add("?task_name", "%" + model.task_desc + "%");
}
// 分页参数
pagePm.Add("?pageoff", offset);
pagePm.Add("?limit", rows);
// 启用状态筛选
whereList.Add("`useflag` = ?useflag");
countPm.Add("?useflag", 1);
pagePm.Add("?useflag", 1);
if (model.useflag == 1) // 显示禁用任务
{
whereList.Add("`useflag` = ?useflag");
countPm.Add("?useflag", 0);
pagePm.Add("?useflag", 0);
}
if (model.useflag == 2) // 显示启用任务
{
whereList.Add("`useflag` = ?useflag");
countPm.Add("?useflag", 1);
pagePm.Add("?useflag", 1);
}
// 执行查询并转换接收人ID为姓名
tasks = _mysqlService.DBQuery<TaskModel>(_qasystem, sqlPage, pagePm);
foreach (TaskModel task in tasks)
{
if (!string.IsNullOrEmpty(task.accept_emp))
{
string[] empIds = task.accept_emp.Split(',');
List<string> empNames = new List<string>();
foreach (string empId in empIds)
{
empNames.Add(DI.QueryEmployeeByEmpCode(_mysqlService, _qasystem, empId).empName);
}
task.accept_emp = string.Join(",", empNames);
}
}
// ... 返回DataTableModel
}
关键技术点:
- 动态SQL构建:使用List组合查询条件,避免SQL注入
- Dapper参数化查询:保证查询安全性
- 接收人ID转姓名:在查询结果中实时转换,提升用户体验
- Layui数据表格协议:返回符合Layui规范的分页数据格式
3.3 任务新增
[HttpPost]
public JsonResult TaskAdd(TaskModel model)
{
MsgModel MsgObj = null;
try
{
model.useflag = 1; // 默认启用
model.dtTime = DateTime.Now; // 设置创建时间
model.accept_emp = string.IsNullOrEmpty(model.accept_emp) ? "" : model.accept_emp;
model.month = string.IsNullOrEmpty(model.month) ? "" : model.month;
string sql = @"INSERT INTO `Tasks` (`task_name`, `task_desc`, `time`, `date`,
`month`,`IntervalTime`,`accept_chat`, `accept_emp`, `accept_rebot`,
`sendType`, `dtTime`, `method`, `handle`, `data_source`, `useflag`)
VALUES (?task_name, ?task_desc, ?time, ?date, ?month, ?IntervalTime,
?accept_chat, ?accept_emp, ?accept_rebot, ?sendType, ?dtTime,
?method, ?handle, ?data_source, ?useflag)";
_mysqlService.DBExecute(_qasystem, sql, model);
MsgObj = new MsgModel { code = 0, msg = "新增任务成功!~~~" };
}
catch (Exception ex)
{
MsgObj = new MsgModel { code = 1, msg = ex.Message };
_logger.LogError(ex.Message);
}
return Json(MsgObj);
}
3.4 任务编辑与删除
[HttpPost]
public JsonResult TaskEdit(TaskModel model)
{
// 更新任务配置
string sql = @"UPDATE `Tasks` SET `task_name`=?task_name, `task_desc`=?task_desc,
`time`=?time, `date`=?date, `month`=?month, `IntervalTime`=?IntervalTime,
`accept_chat`=?accept_chat, `accept_emp`=?accept_emp,
`accept_rebot`=?accept_rebot,`sendType`=?sendType, `method`=?method,
`handle`=?handle,`data_source`=?data_source WHERE `id`=?id";
_mysqlService.DBExecute(_qasystem, sql, model);
return Json(new MsgModel { code = 0, msg = "操作成功" });
}
[HttpPost]
public JsonResult TaskDelete(TaskModel model)
{
string sql = "DELETE FROM `Tasks` WHERE `id`=?id";
_mysqlService.DBExecute(_qasystem, sql, model);
return Json(new MsgModel { code = 0, msg = "操作成功" });
}
[HttpPost]
public JsonResult TaskCancel(TaskModel model)
{
// 启用/禁用任务切换
string sql = "UPDATE `Tasks` SET `useflag` = ?useflag WHERE `id` = ?id";
_mysqlService.DBExecute(_qasystem, sql, new TaskModel { useflag = model.useflag, id = model.id });
return Json(new MsgModel { code = 0, msg = "操作成功" });
}
四、前端视图实现
4.1 任务列表主页面
任务列表页面采用Layui数据表格实现,核心代码如下:
table.render({
elem: '#TaskTable'
, url: '/Task/TaskList'
, cols: [[
{ type: 'checkbox', fixed: 'left' }
, { field: 'id', width: 80, align: 'center', title: '序号' }
, { field: 'useflag', width: 90, align: 'center', title: '启用',
templet: '#useflag', unresize: true }
, { field: 'task_status', width: 85, align: 'center', title: '状态',
templet: '#task_status' }
, { field: 'method', width: 120, align: 'center', title: '处理方式',
templet: '#method' }
, { field: 'task_desc', width: 250, align: 'left', title: '任务描述' }
, { field: 'task_name', width: 200, align: 'left', title: '任务名称' }
, { field: 'updateTime', width: 180, align: 'center', title: '心跳时间',
templet: '#updateTime' }
, { field: 'time', width: 90, align: 'center', title: '触发时间' }
, { field: 'accept_emp', width: 180, align: 'center', title: '接收人' }
, { title: '操作', width: 120, align: 'center', fixed: 'right',
toolbar: '#table-shortcut-manager' }
]]
, page: true
, limit: 20
, height: 'full-220'
});

表格列设计说明:
| 列名 | 宽度 | 说明 |
|---|---|---|
| 序号 | 80px | 数据主键 |
| 启用 | 90px | 开关组件,支持快速启用/禁用 |
| 状态 | 85px | 未触发/成功/失败三种状态 |
| 处理方式 | 120px | 内部处理/SQL脚本/GET/POST |
| 任务描述 | 250px | 主要识别信息 |
| 任务名称 | 200px | 代码中的方法名 |
| 心跳时间 | 180px | 最后执行时间 |
| 触发时间 | 90px | 计划执行时间 |
| 接收人 | 180px | 钉钉通知接收人 |
| 操作 | 120px | 编辑按钮 |
4.2 状态可视化模板
// 任务状态模板
script id="task_status" type="text/html">
{{# if(d.task_status == "0" && d.updateTime == null){ }}
<button class="layui-btn layui-btn-danger layui-btn-xs" type="button">未触发</button>
{{# }else if(d.task_status == "1"){ }}
<button class="layui-btn layui-btn layui-btn-xs" type="button"
data-id="{{d.id}}" data-title="{{d.task_desc}}" lay-on="show_log">成功</button>
{{# }else{ }}
<button class="layui-btn layui-btn-danger layui-btn-xs" type="button"
data-id="{{d.id}}" data-title="{{d.task_desc}}" lay-on="show_log">失败</button>
{{# } }}
</script>
// 处理方式模板
script id="method" type="text/html">
{{# if(d.method == "0" ){ }}
<button class="layui-btn layui-bg-orange layui-btn-xs" type="button">内部处理</button>
{{# }else if(d.method == "1"){ }}
<button class="layui-btn layui-bg-purple layui-btn-xs" data-id="{{d.id}}"
data-title="{{d.task_desc}}" lay-on="show_sql" type="button">SQL脚本</button>
{{# }else if(d.method == "2"){ }}
<button class="layui-btn layui-bg-blue layui-btn-xs" data-id="{{d.handle}}"
data-title="{{d.task_desc}}" lay-on="show_url" type="button">GET方式</button>
{{# }else if(d.method == "3"){ }}
<button class="layui-btn layui-bg-blue layui-btn-xs" data-id="{{d.handle}}"
data-title="{{d.task_desc}}" lay-on="show_url" type="button">POST方式</button>
{{# }}}
</script>
4.3 任务新增页面
任务新增页面采用分块表单设计,将复杂配置按业务逻辑分组:
<fieldset class="layui-elem-field">
<legend>任务名称描述</legend>
<div class="layui-field-box">
<!-- 任务名称和描述输入 -->
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>任务触发设置</legend>
<div class="layui-field-box">
<!-- 触发时间、日期、月份、间隔时间 -->
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>钉钉消息推送</legend>
<div class="layui-field-box">
<!-- 钉钉群ID、机器人、接收人、推送类型 -->
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend>任务处理设置</legend>
<div class="layui-field-box">
<!-- 处理方式、数据源、具体处理内容 -->
</div>
</fieldset>
表单动态联动:
当选择不同处理方式时,表单会动态显示不同的输入控件:
form.on('select(method-select-filter)', function (data) {
var value = data.value;
if(value == "0"){
// 内部程序:无需额外配置
$("#handle-id").hide();
$("#data_source_select_elem").html("");
}else if(value == "1"){
// SQL脚本:显示文本域和数据源选择
$("#handle-id").show();
$("#handle-elem").html('<textarea name="handle" placeholder="具体处理脚本" class="layui-textarea"></textarea>');
// 动态加载数据源选项...
}else if(value == "2"){
// GET方式:显示URL输入框
$("#handle-elem").html('<input type="text" name="handle" placeholder="请输入GET地址" class="layui-input">');
}else if(value == "3"){
// POST方式:显示URL输入框
$("#handle-elem").html('<input name="handle" placeholder="请输入POST地址" class="layui-input">');
}
});
4.4 任务日志查看
日志查看采用Layui Tab组件展示,每次任务执行生成一条日志记录:
<div class="layui-tab layui-tab-card">
<ul class="layui-tab-title">
@foreach (TaskLogModel log in TaskLogs)
{
<li>{{log.record_time.ToString("yyyy-MM-dd HH:mm:ss")}}</li>
}
</ul>
<div class="layui-tab-content">
@foreach (TaskLogModel log in TaskLogs)
{
<div class="layui-tab-item">
<pre class="layui-code code-demo">
{{log.record_content}}
</pre>
</div>
}
</div>
</div>

五、核心功能实现详解
5.1 多数据源切换
医院信息系统通常包含多个业务数据库,系统支持灵活的数据源配置:
// TaskController中的数据源配置
_qasystem = configuration.GetConnectionString("qasystem"); // QA系统数据库
_harmfulevent = configuration.GetConnectionString("harmfulevent"); // 不良事件数据库
_deepthroat = configuration.GetConnectionString("deepthroat"); // 深喉数据库
// 查询时指定数据源
tasks = _mysqlService.DBQuery<TaskModel>(_qasystem, sqlPage, pagePm);
医疗场景应用:
- 联众HIS:存储患者诊疗信息、医嘱等核心业务数据
- 矽岛EMR:电子病历系统,存储病历文书数据
- QA系统:质控系统,存储任务配置和日志
5.2 钉钉消息推送配置
系统支持配置化的钉钉群消息推送:
// 任务配置中的钉钉相关字段
public string accept_chat { get; set; } // 钉钉群ID
public string accept_rebot { get; set; } // 机器人Webhook
public string accept_emp { get; set; } // 指定接收人
public string sendType { get; set; } // 推送类型(成功/失败)
前端通过xm-select组件实现多选:
var demo1 = xmSelect.render({
el: '#accept_emp',
name: 'accept_emp',
language: 'zn',
filterable: true, // 支持搜索过滤
autoRow: true, // 自动换行
paging: true, // 支持分页
pageSize: 20,
data: @Html.Raw(ViewData["emps"]) // 从后端加载员工数据
});
5.3 灵活的触发策略
系统支持多种触发策略的组合配置:
// 触发时间:每天的固定时间点
public string time { get; set; } // "HH:mm" 格式,如 "08:30"
// 触发日期:一个月的哪些天
public string date { get; set; } // "01,05,10,15,20,25"
// 触发月份:哪些月份执行
public string month { get; set; } // "01,02,03,04" 格式
// 间隔执行:每隔N分钟执行一次
public int IntervalTime { get; set; } // 单位:分钟
典型医疗业务场景:
| 场景 | 时间 | 日期 | 月份 | 间隔 |
|---|---|---|---|---|
| 每日晨报 | 07:30 | * | * | - |
| 月度报表 | 08:00 | 01 | * | - |
| 季度分析 | 09:00 | 01 | 01,04,07,10 | - |
| 危急值监控 | - | * | * | 5分钟 |
5.4 批量操作与快速切换
// 批量删除任务
, TaskDelete: function () {
var checkStatus = table.checkStatus('TaskTable')
checkData = checkStatus.data;
if (checkData.length === 0) {
return layer.msg('请选择任务');
}
layer.confirm('确定删除吗?', function (index) {
for (var i = 0; i < checkData.length; i++) {
$.post("/Task/TaskDelete", checkData[i], function (data) {
layer.msg(data.msg);
table.reload('TaskTable');
});
}
layer.close(index);
});
}
// 快速启用/禁用
form.on('switch(useflag)', function (obj) {
var postObj = {
id: this.value,
useflag: obj.elem.checked ? 1 : 0
};
$.post("/Task/TaskCancel", postObj, function (data) {
layer.msg(data.msg);
table.reload('TaskTable');
});
});
六、用户体验优化
6.1 搜索与筛选
// 按任务描述搜索
form.on('submit(TaskSearch)', function (data) {
table.reload('TaskTable', {
where: data.field,
page: { curr: 1 }
});
});
// 按启用状态筛选
form.on('switch(demo-checkbox-filter)', function(data){
var postObj = {
useflag: checked ? 2 : 1 // 2显示启用,1显示禁用
};
table.reload('TaskTable', {
where: postObj,
page: { curr: 1 }
});
});
6.2 弹窗交互
// 任务编辑弹窗
table.on('tool(TaskTable)', function (obj) {
if (obj.event === 'TaskEdit') {
layer.open({
type: 2
, title: '编辑'
, content: '/Task/TaskEdit?id=' + data.id
, maxmin: true
, area: ['90%', '90%']
, btn: ['确定', '取消']
, yes: function (index, layero) {
var iframeWindow = window['layui-layer-iframe' + index]
, submitID = 'task-edit-submit'
, submit = layero.find('iframe').contents().find('#' + submitID);
iframeWindow.layui.form.on('submit(' + submitID + ')', function (data) {
$.post("/Task/TaskEdit", data.field, function (data) {
layer.msg(data.msg);
table.reload('TaskTable');
});
layer.close(index);
});
submit.trigger('click');
}
});
}
});
## 七、安全性设计
### 7.1 身份验证
所有TaskController的操作都继承自CheckLoginController,确保用户已登录:
```csharp
public class TaskController : CheckLoginController
{
// 所有操作都需要登录后才能访问
}
7.2 SQL注入防护
系统采用Dapper参数化查询,有效防止SQL注入:
// 使用参数化查询
countPm.Add("?task_desc", "%" + model.task_desc + "%");
// 生成的SQL:WHERE task_desc LIKE ?task_desc
// 参数值会被正确转义
7.3 操作日志审计
关键操作都有日志记录:
catch (Exception ex)
{
MsgObj = new MsgModel { code = 1, msg = ex.Message };
_logger.LogError(ex.Message); // 记录错误日志
}
八、部署与运维
8.1 环境配置
{
"ConnectionStrings": {
"qasystem": "Server=localhost;Database=qasystem;Uid=root;Pwd=xxx;",
"harmfulevent": "Server=localhost;Database=harmfulevent;Uid=root;Pwd=xxx;",
"deepthroat": "Server=localhost;Database=deepthroat;Uid=root;Pwd=xxx;"
}
}
8.2 监控指标
建议监控以下关键指标:
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| 任务失败率 | 失败任务数/总任务数 | >5% |
| 平均执行时间 | 任务平均耗时 | >30秒 |
| 未触发任务 | 超过计划时间2小时未执行 | >0 |
| 积压任务数 | 等待执行的任务数 | >100 |
九、总结
本文详细介绍了医院定时任务服务Web管理后台的设计与实现,主要包括以下内容:
9.1 架构设计
- 采用ASP.NET Core + Layui的前后端分离架构
- 通过Dapper实现高性能数据库访问
- 支持多数据源配置,适应医院复杂的信息系统环境
9.2 核心功能
- 任务列表:支持分页、搜索、状态筛选、批量操作
- 任务配置:灵活的多维度触发策略配置
- 钉钉集成:支持群消息推送和指定接收人
- 日志追溯:完整的执行记录和状态追踪
9.3 医疗业务融合
系统紧密结合医院实际业务场景:
- 支持多数据源(HIS、EMR等)配置
- 符合医疗信息安全要求的审计日志
- 满足危急值监控等时效性要求
9.4 未来优化方向
- 实时推送:接入WebSocket实现任务状态的实时更新
- 可视化调度:提供任务依赖关系的图形化配置
- 智能预警:基于历史数据分析,预测任务执行异常
- 移动端支持:开发移动端管理APP,提升运维效率
如果本文对你有帮助,欢迎点赞、收藏、评论交流!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)