为医院量身定制:智能定时任务服务的架构设计与实现(下)

写在前面:本文是定时任务服务系列的下篇,重点介绍定时任务的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
}

关键技术点

  1. 动态SQL构建:使用List组合查询条件,避免SQL注入
  2. Dapper参数化查询:保证查询安全性
  3. 接收人ID转姓名:在查询结果中实时转换,提升用户体验
  4. 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 未来优化方向

  1. 实时推送:接入WebSocket实现任务状态的实时更新
  2. 可视化调度:提供任务依赖关系的图形化配置
  3. 智能预警:基于历史数据分析,预测任务执行异常
  4. 移动端支持:开发移动端管理APP,提升运维效率

如果本文对你有帮助,欢迎点赞、收藏、评论交流!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐