医院自律端系统——预警处置模块全栈实战(ASP.NET Core + Vue3 + Quartz 定时调度)

医院自律端系统------预警处置模块全栈实战(ASP.NET Core + Vue3 + Quartz 定时调度)

一、项目背景

医院自律端系统是面向医疗机构的内控监管平台,核心目标是对药品、耗材、设备 三大领域的异常行为进行实时监控与预警。当某科室/医疗组的药品使用金额出现环比/同比异常增长时,系统自动生成预警记录,管理员进行线上处置,形成"计算 → 预警 → 处置 → 闭环"的管理链路。

本文以预警处置模块(WarnDispositions)为切入点,深入分析其前端交互设计、后端 API 设计、预警计算引擎与 Quartz 定时调度机制的完整实现。

二、技术栈概览

层级 技术选型
前端 Vue 3 (Composition API) + Vite + Element Plus + Vue Router
后端 ASP.NET Core (.NET 10) + EF Core
数据库 MySQL(业务库) + PostgreSQL(HIS 视图只读)
定时调度 Quartz.NET
认证鉴权 JWT + BCrypt
日志 Serilog
部署 IIS + Nginx(前端静态资源)

三、预警处置模块前端实现

3.1 页面结构

预警处置页面(WarnDispositions.vue(file:///d:/HospitalSelfDiscipline/hospitalselfdiscipline.web/src/views/warn/WarnDispositions.vue))分为四个核心区域:

  1. 搜索筛选区 --- 按预警指标、处置状态筛选
  2. 数据列表区 --- 分页展示预警处置记录
  3. 详情弹窗 --- 查看预警完整信息 + 环比/同比金额明细
  4. 处置弹窗 --- 管理员填写处置意见并更新状态

3.2 搜索筛选

使用 el-form 行内表单,提供两级筛选:

html 复制代码
<el-form :model="searchForm" inline>
  <el-form-item label="预警指标">
    <el-select v-model="searchForm.indicatorId" placeholder="请选择">
      <el-option label="全部" :value="0" />
      <el-option v-for="item in indicators" :key="item.id" 
                 :label="item.indicatorName" :value="item.id" />
    </el-select>
  </el-form-item>
  <el-form-item label="处置状态">
    <el-select v-model="searchForm.status" placeholder="请选择">
      <el-option label="全部" :value="-1" />
      <el-option label="待处置" :value="0" />
      <el-option label="核查中" :value="1" />
      <el-option label="持续监测" :value="2" />
      <el-option label="专项点评" :value="3" />
      <el-option label="已办结" :value="4" />
    </el-select>
  </el-form-item>
  <el-form-item>
    <el-button type="primary" @click="handleSearch">查询</el-button>
    <el-button @click="handleReset">重置</el-button>
  </el-form-item>
</el-form>

设计亮点: 使用 reactive 而非多个 ref 管理表单状态,状态切换时重置分页回到第 1 页,避免"翻到第 5 页后筛选结果为空"的体验问题。

3.3 状态可视化

预警级别和处置状态使用语义化色彩标签(映射函数 + CSS 类名):

javascript 复制代码
const getLevelClass = (level) => {
  const classes = { 1: 'level-serious', 2: 'level-critical' }
  return `level-tag ${classes[level] || 'level-info'}`
}

const getLevelText = (level) => {
  const texts = { 1: '黄色预警', 2: '红色预警' }
  return texts[level] || '未知'
}

CSS 色卡:

scss 复制代码
.level-critical { background: #fef0f0; color: #dc2626; }   // 红色预警
.level-serious  { background: #fff7ed; color: #ea580c; }   // 黄色预警
.status-pending    { background: #f0f9ff; color: #0284c7; } // 待处置
.status-checking   { background: #fef3c7; color: #d97706; } // 核查中
.status-monitoring { background: #ede9fe; color: #7c3aed; } // 持续监测
.status-review     { background: #fce7f3; color: #db2777; } // 专项点评
.status-completed  { background: #d1fae5; color: #059669; } // 已办结

3.4 环比/同比金额明细查询

详情弹窗中,根据预警的对比类型MOM 环比 / YOY 同比)动态展示当期与对比期金额,并支持"查看明细"按钮打开费用明细弹窗:

html 复制代码
<template v-if="detailForm.compareType === 'MOM'">
  <el-form-item label="本月金额(元)">
    <span>{{ detailForm.currentAmount }}</span>
    <el-button size="small" type="primary" @click="openFeeDetail('current', detailForm)">
      查看明细
    </el-button>
  </el-form-item>
  <el-form-item label="上月同期(元)">
    <span>{{ detailForm.prevAmount }}</span>
    <el-button size="small" type="primary" @click="openFeeDetail('prev', detailForm)">
      查看明细
    </el-button>
  </el-form-item>
</template>

费用明细弹窗通过 /drug-usage/his/detail/{warnRecordId} 接口获取 HIS 原始计费记录,表格展示药品名称、科室、医疗组、金额、计费日期,底部汇总合计金额。

3.5 处置提交

管理员在处置弹窗中选择状态、填写意见后提交:

javascript 复制代码
const submitDisposition = async () => {
  if (!dispositionFormRef.value) return
  try {
    await dispositionFormRef.value.validate()
  } catch { return }

  dispositionSubmitting.value = true
  try {
    await request.post('/warn-dispositions', {
      warnRecordId: dispositionForm.warnRecordId,
      dispositionType: dispositionForm.dispositionStatus,
      dispositionAction: dispositionForm.opinion,
      dispositionContent: dispositionForm.opinion
    })
    ElMessage.success('处置成功')
    dispositionVisible.value = false
    loadData()  // 刷新列表
  } catch (e) {
    ElMessage.error(e?.response?.data?.message || '处置失败')
  } finally {
    dispositionSubmitting.value = false
  }
}

四、后端 API 设计

4.1 Controller 层

WarnDispositionController.cs(file:///d:/HospitalSelfDiscipline/HospitalSelfDiscipline/Controllers/WarnDispositionController.cs#L9-L83) 提供三个端点:

方法 路由 功能
POST /api/warn-dispositions 创建处置记录
GET /api/warn-dispositions 分页查询处置列表
PUT /api/warn-dispositions/{id} 更新处置信息

创建处置时,通过 JWT 自动获取当前操作人:

csharp 复制代码
[HttpPost]
public async Task<ActionResult<ApiResponse<WarnDisposition>>> Create([FromBody] WarnDisposition disposition)
{
    disposition.OperatorId = GetCurrentUserId();
    disposition.DeptCode = GetCurrentUserDeptCode();
    var created = await _service.CreateAsync(disposition);
    return Ok(ApiResponse<WarnDisposition>.Success(created, "处置成功"));
}

设计要点: 不信任前端传入的操作人信息,从 JWT Token 中提取 userIddeptId Claim,防止伪造。

4.2 Service 层 --- 关联查询优化

WarnDispositionService.cs(file:///d:/HospitalSelfDiscipline/HospitalSelfDiscipline/Services/WarnDispositionService.cs#L56-L198) 的 GetListAsync 方法是一个典型的多表关联投影查询

处置记录本身只存储 WarnRecordId,列表展示需要从 warn_recordwarn_indicatorsys_departmentsys_medical_groupwarn_threshold 等关联表获取完整信息。使用 EF Core 的 Select 投影一次性获取所需字段:

csharp 复制代码
var items = await query
    .OrderByDescending(d => d.CreateTime)
    .Skip((page - 1) * pageSize)
    .Take(pageSize)
    .Select(d => new
    {
        d.Id,
        d.WarnRecordId,
        indicatorName = _db.WarnIndicators
            .Where(i => i.Id == _db.WarnRecords
                .Where(r => r.Id == d.WarnRecordId)
                .Select(r => r.IndicatorId).FirstOrDefault())
            .Select(i => i.IndicatorName).FirstOrDefault(),
        departmentName = _db.SysDepartments
            .Where(dp => dp.DeptCode == _db.WarnRecords
                .Where(r => r.Id == d.WarnRecordId)
                .Select(r => r.DeptCode).FirstOrDefault())
            .Select(dp => dp.DeptName).FirstOrDefault(),
        // ... 其他字段
    })
    .ToListAsync();

4.3 Entity 设计

WarnDisposition.cs(file:///d:/HospitalSelfDiscipline/HospitalSelfDiscipline/Entities/WarnDisposition.cs#L6-L53) 实体与数据库表一一映射:

csharp 复制代码
public class WarnDisposition
{
    public long Id { get; set; }
    public long WarnRecordId { get; set; }        // 关联预警记录
    public int DispositionType { get; set; }       // 处置类型(0-5)
    public string DispositionAction { get; set; }  // 处置动作
    public string? DispositionContent { get; set; } // 处置意见
    public string? DispositionResult { get; set; }  // 处置结果
    public long OperatorId { get; set; }            // 操作人
    public string? DeptCode { get; set; }           // 归属科室
    // 后续跟进相关字段
    public string? CheckCause { get; set; }
    public sbyte? IsFurtherCheck { get; set; }
    public string? FollowUpAction { get; set; }
    public DateOnly? FollowUpDeadline { get; set; }
    public sbyte? NextMonthStatus { get; set; }
    public DateTime? CreateTime { get; set; }
}

WarnRecord.cs(file:///d:/HospitalSelfDiscipline/HospitalSelfDiscipline/Entities/WarnRecord.cs#L6-L93) 作为预警记录核心实体,包含了环比/同比的计算结果、阈值对比、处置状态、查询参数快照等完整信息:

csharp 复制代码
public class WarnRecord
{
    public long Id { get; set; }
    public long IndicatorId { get; set; }        // 预警指标
    public long ScenarioId { get; set; }          // 预警场景
    public string WarnCode { get; set; }          // 预警编号 (如 WARN-DRUG-202606-143022-0001)
    public sbyte WarnLevel { get; set; }           // 1=黄色 2=红色
    public decimal CurrentValue { get; set; }      // 当前值(增长率%)
    public decimal ThresholdValue { get; set; }    // 触发阈值
    public decimal? GrowthRate { get; set; }       // 增长率
    public decimal? CurrentAmount { get; set; }    // 当期金额
    public decimal? PrevAmount { get; set; }       // 上期金额(环比)
    public decimal? PrevYearAmount { get; set; }   // 去年同期金额(同比)
    public string? CompareType { get; set; }       // MOM / YOY
    public sbyte? DispositionStatus { get; set; }  // 处置状态
    public DateTime? DispositionDeadline { get; set; } // 处置截止时间(7天)
    public string? QueryParams { get; set; }       // JSON快照,保存查询条件用于明细回溯
}

五、预警计算引擎 ------ 系统心脏

5.1 整体架构

复制代码
┌──────────────────────────────────────────┐
│              Quartz Scheduler            │
│  WarnCalculationJob (Cron: 0 0 * * * ?)  │
│           每小时执行一次                     │
└──────────────────┬───────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────┐
│         WarnCalculationService           │
│                                          │
│  CalculateAllWarnings() ─────────────────┤
│    ├─ 遍历所有启用指标 (Status=1)           │
│    ├─ 遍历每个指标的阈值配置                 │
│    └─ ProcessIndicatorThreshold()        │
│         ├─ DRUG_KEY_TOTAL → 重点药品合计   │
│         ├─ DRUG_KEY_NATION → 国家监控      │
│         ├─ DRUG_KEY_PROVINCE → 省监控     │
│         ├─ DRUG_KEY_BATCH1/2 → 批次监控   │
│         ├─ DRUG_KEY_JIANDE → 建德监控     │
│         ├─ DRUG_NEW_APPLY → 新药申请      │
│         ├─ DRUG_TURNOVER → 周转率         │
│         ├─ CONS_TEMP_APPLY → 临采申请     │
│         ├─ EQUIP_PURCHASE_BUDGET → 采购   │
│         └─ ...共 20+ 指标                  │
└──────────────────┬───────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────┐
│     CheckAndGenerateWarn()               │
│  ├─ 阈值评估 (EvaluateThreshold)          │
│  │   ├─ GT: currentValue > red → 红色     │
│  │   ├─ GT: currentValue > yellow → 黄色  │
│  │   ├─ LT: currentValue < red → 红色     │
│  │   └─ BETWEEN: 区间判定                 │
│  ├─ 主键去重 (indicator + hospital +      │
│  │           dept + group + month)        │
│  ├─ 存在 → 更新级别和当前值               │
│  └─ 不存在 → 创建新记录 + 生成编号         │
└──────────────────────────────────────────┘

5.2 阈值评估算法

WarnCalculationService.cs(file:///d:/HospitalSelfDiscipline/HospitalSelfDiscipline/Services/WarnCalculationService.cs#L338-L361) 中的阈值评估支持三种比较模式:

csharp 复制代码
private static sbyte EvaluateThreshold(decimal currentValue, WarnThreshold threshold)
{
    var type = threshold.ThresholdType?.ToUpper() ?? "GT";
    var yellow = threshold.YellowThreshold;
    var red = threshold.RedThreshold;

    switch (type)
    {
        case "GT":       // 大于 → 适用于增长率指标
            if (currentValue > red) return 2;     // 红色预警
            if (currentValue > yellow) return 1;  // 黄色预警
            break;
        case "LT":       // 小于 → 适用于设备使用率等
            if (currentValue < red) return 2;
            if (currentValue < yellow) return 1;
            break;
        case "BETWEEN":  // 区间 → 在黄红之间触发
            if (currentValue >= yellow && currentValue <= red) return 1;
            if (currentValue > red) return 2;
            break;
    }
    return 0; // 正常,不触发预警
}

5.3 预警编号生成

使用 Interlocked.Increment 原子计数器 + 时间戳 生成全局唯一预警编号:

csharp 复制代码
private static int _warnSeq;

var seq = Interlocked.Increment(ref _warnSeq);
var timestamp = DateTime.Now.ToString("HHmmssfff");
var warnCode = $"WARN-{scenarioCode}-{warnMonth.Replace("-", "")}-{timestamp}-{seq:D4}";
// 示例: WARN-DRUG-202606-143022567-0001

相比传统的 Count() + 1 方案,Interlocked 保证了多线程并发场景下编号绝对不重复。

5.4 HIS 数据查询优化

预警计算的核心数据源是医院 HIS 系统的 v_zlxt_drug_usage_record 视图(PostgreSQL)。查询按医疗组 + 科室 + 药品属性三维聚合:

csharp 复制代码
private async Task<List<GroupDeptAttrAggregation>> QueryHisDrugUsageByGroupDeptAttr(
    DateTime startDate, DateTime endDate, long hospitalId, List<string> attrCodes)
{
    _hisDb.Database.SetCommandTimeout(300);  // 大数据量查询超时设置

    var query = _hisDb.VDrugUsageRecords.AsQueryable();
    query = query.Where(v => v.Jifeisj >= startDate && v.Jifeisj <= endDate);
    
    if (attrCodes.Count > 0)
        query = query.Where(v => attrCodes.Contains(v.Shuxingdm));
    
    // 医疗组必须有值(过滤掉未分配医疗组的记录)
    query = query.Where(v => v.Kaidanylzid != null && v.Kaidanylzid != "");

    // 按医疗组+科室+药品属性分组
    return await query
        .GroupBy(v => new { v.Kaidanylzid, v.Kaidanylzmc, 
                            v.Kaidanksid, v.Kaidanksmc, v.Shuxingdm })
        .Select(g => new GroupDeptAttrAggregation
        {
            GroupHisId = g.Key.Kaidanylzid,
            GroupName = g.Key.Kaidanylzmc ?? "",
            DeptHisId = g.Key.Kaidanksid?.ToString() ?? "",
            DeptName = g.Key.Kaidanksmc ?? "",
            AttrCode = g.Key.Shuxingdm,
            TotalAmount = g.Sum(v => v.Jine)
        })
        .Where(a => a.TotalAmount > 0)  // HAVING SUM(jine) > 0
        .ToListAsync();
}

查询性能要点:

  • 设置 300s 超时,避免 HIS 视图查询超时异常
  • 数据库端完成聚合(GROUP BY + SUM),减少网络传输
  • 增加 WHERE TotalAmount > 0 过滤无效分组

5.5 环比/同比计算

以重点监控药品金额(DrugKeyMonitorGrowth)为例,一次查询获取三个时段的数据(本期、上月同期、去年同期),同时计算环比和同比:

csharp 复制代码
private async Task CalculateGroupDeptGrowth(...)
{
    foreach (var item in currentData)
    {
        var currentTotal = item.TotalAmount;
        var prevTotal = prevData.FirstOrDefault(...)?.TotalAmount ?? 0;
        var prevYearTotal = prevYearData.FirstOrDefault(...)?.TotalAmount ?? 0;

        // 环比预警
        if (prevTotal > 0)
        {
            var momGrowth = (currentTotal - prevTotal) / prevTotal;
            await CheckAndGenerateWarn(indicator, threshold, ...,
                currentAmount: currentTotal, prevAmount: prevTotal,
                compareType: "MOM",
                description: $"{groupLabel}[{groupName}] 环比增长: {momGrowth:P2}");
        }

        // 同比预警
        if (prevYearTotal > 0)
        {
            var yoyGrowth = (currentTotal - prevYearTotal) / prevYearTotal;
            await CheckAndGenerateWarn(indicator, threshold, ...,
                currentAmount: currentTotal, prevYearAmount: prevYearTotal,
                compareType: "YOY",
                description: $"{groupLabel}[{groupName}] 同比增长: {yoyGrowth:P2}");
        }
    }
}

预警记录去重策略:IndicatorId + HospitalId + DeptCode + MedicalGroupId + CompareType + WarnMonth 六元组作为业务主键。已存在的记录更新预警级别(可能从黄色升级到红色),不存在的创建新记录。

5.6 已办结复现机制

一个巧妙的设计:当某条预警已办结/已归档后,如果下月又触发相同维度的预警,系统自动将处置状态重置为"待处置"并重新设定 7 天处置截止时间:

csharp 复制代码
if (existingRecord.DispositionStatus == 4 || existingRecord.DispositionStatus == 5)
{
    existingRecord.DispositionStatus = 0;
    existingRecord.IsOvertime = 0;
    existingRecord.DispositionDeadline = DateTime.Now.AddDays(7);
}

六、Quartz 定时调度

6.1 任务注册

Program.cs(file:///d:/HospitalSelfDiscipline/HospitalSelfDiscipline/Program.cs#L179-L187) 中注册 Quartz 服务并加载数据库中的任务配置:

csharp 复制代码
builder.Services.AddQuartz(q =>
{
    q.AddJobListener<JobExecutionListener>();
});
builder.Services.AddQuartzHostedService(options =>
{
    options.WaitForJobsToComplete = true;
});

启动时从 sys_quartz_job 表读取启用的任务:

csharp 复制代码
var jobConfigs = db.SysQuartzJobs.Where(j => j.Status == 1).ToList();
foreach (var jobConfig in jobConfigs)
{
    var job = JobBuilder.Create<DynamicJob>().WithIdentity(jobKey).Build();
    ITrigger trigger = jobConfig.TriggerType == "cron"
        ? TriggerBuilder.Create().WithCronSchedule(jobConfig.CronExpr).Build()
        : TriggerBuilder.Create().WithSimpleSchedule(...).Build();
    await scheduler.ScheduleJob(job, trigger);
}

6.2 核心任务

任务 Cron 说明
WarnCalculationJob 0 0 * * * ? 每小时执行一次预警计算
DepartmentSyncJob 0 0 5 * * ? 每天 5:00 从 HIS 同步科室
MedicalGroupSyncJob 0 0 5 * * ? 每天 5:00 从 HIS 同步医疗组

6.3 任务执行监听

通过 JobExecutionListener 记录每次任务执行的开始时间、结束时间、耗时、成功/失败状态到 sys_quartz_job_log 表,形成完整的调度审计日志。

七、前端路由与权限控制

router/index.js(file:///d:/HospitalSelfDiscipline/hospitalselfdiscipline.web/src/router/index.js) 使用 Vue Router 懒加载所有页面:

javascript 复制代码
{
  path: 'warn-dispositions',
  name: 'WarnDispositions',
  component: () => import('@/views/warn/WarnDispositions.vue')
}

路由守卫检查 localStorage 中的 JWT Token,未登录自动跳转登录页。菜单权限通过后端 sys_permission 表控制,前端布局根据接口返回的权限列表动态渲染侧边栏。

八、架构亮点总结

  1. 全维度预警覆盖 --- 药品(20+ 指标)、耗材(5 指标)、设备(5 指标),支持环比/同比双模式
  2. 阈值灵活配置 --- 支持 GT/LT/BETWEEN 三种比较类型,黄色/红色双级预警,可按医院、科室、医疗组粒度配置
  3. 查询参数快照 --- WarnRecord.QueryParams 存储为 JSON,支持回溯查询原始数据明细,无需重新计算日期范围
  4. 处置闭环管理 --- 待处置 → 核查中 → 已办结,超时自动标记,办结后复发自动重置
  5. 双数据库架构 --- MySQL 存业务数据,PostgreSQL 只读 HIS 视图,计算时批量聚合减少跨库查询
  6. 定时计算 + 前端轮询 --- 每小时自动计算,前端按需查询,成本可控
  7. 并发安全 --- Interlocked.Increment 保证编号唯一性,EF Core 投影查询减少数据传输

九、结语

医院自律端系统通过 ASP.NET Core + Vue 3 + Quartz.NET 的技术组合,实现了一套完整的医院内部监管预警体系。预警处置模块作为整个闭环的"最后一公里",其设计兼顾了操作便捷性与数据完整性。本文的源码分析希望能为类似场景的全栈开发提供参考。

相关推荐
IvorySQL3 小时前
PostgreSQL 技术日报 (6月9日)|PL/SQL 迁移自动化,前沿峰会即将启幕
sql·postgresql·自动化
睡不醒男孩0308233 小时前
第八篇:如何构建一站式 PostgreSQL 性能优化与智能管控平台?从盲目排查到 CLup 自动化运维演进
运维·postgresql·性能优化
睡不醒男孩0308233 小时前
第五篇:2026年企业级 PostgreSQL 高可用方案深度横评:Patroni vs. CLup 架构与可靠性全面对决
数据库·postgresql·架构
NineData3 小时前
SQL 都在等锁时,ChatDBA 先帮 MySQL 找到谁在挡路
数据库·人工智能·sql·mysql·安全·数据复制·数据迁移工具
神仙别闹3 小时前
基于 PHP + MySQL学生信息管理系统
android·mysql·php
Amnesia0_03 小时前
MYSQL复合查询和内外连接
数据库·mysql
Fanta丶3 小时前
22.MySql order by优化
mysql
哆啦A梦——4 小时前
Ubuntu 虚拟机 Docker 与 MySQL 8.0.42 部署指南
mysql·ubuntu·docker
大大杰哥5 小时前
Vue2学习(1)--了解基本方法与概念
javascript·学习·vue