feat(ybmanage): add DRG/DIP deep analysis module (T13.5)

This commit is contained in:
2026-06-18 16:03:09 +08:00
parent 46ae0f39ab
commit b682bde47f
5 changed files with 423 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
package com.healthlink.his.web.ybmanage.appservice;
import com.core.common.core.domain.R;
import java.util.Map;
public interface IDrgDeepAppService {
R<?> analyzeDrg(Map<String, Object> params);
R<?> getCostAlert(Map<String, Object> params);
R<?> getOptimization(Map<String, Object> params);
}

View File

@@ -0,0 +1,160 @@
package com.healthlink.his.web.ybmanage.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.core.common.core.domain.R;
import com.healthlink.his.crossmodule.domain.DrgCostAlert;
import com.healthlink.his.crossmodule.domain.DrgPerformance;
import com.healthlink.his.crossmodule.service.IDrgCostAlertService;
import com.healthlink.his.crossmodule.service.IDrgPerformanceService;
import com.healthlink.his.mrhomepage.domain.MrDrgGrouping;
import com.healthlink.his.mrhomepage.service.IMrDrgGroupingService;
import com.healthlink.his.web.ybmanage.appservice.IDrgDeepAppService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
@Service
@AllArgsConstructor
public class DrgDeepAppServiceImpl implements IDrgDeepAppService {
private final IMrDrgGroupingService drgGroupingService;
private final IDrgPerformanceService drgPerformanceService;
private final IDrgCostAlertService drgCostAlertService;
@Override
public R<?> analyzeDrg(Map<String, Object> params) {
String startDate = (String) params.get("startDate");
String endDate = (String) params.get("endDate");
String groupingType = (String) params.get("groupingType");
LambdaQueryWrapper<MrDrgGrouping> w = new LambdaQueryWrapper<>();
w.eq(StringUtils.hasText(groupingType), MrDrgGrouping::getGroupingType, groupingType)
.eq(MrDrgGrouping::getIsValid, true);
if (StringUtils.hasText(startDate)) {
w.ge(MrDrgGrouping::getDischargeDate, startDate);
}
if (StringUtils.hasText(endDate)) {
w.le(MrDrgGrouping::getDischargeDate, endDate);
}
List<MrDrgGrouping> list = drgGroupingService.list(w);
Map<String, Object> result = new HashMap<>();
result.put("totalCases", list.size());
BigDecimal totalCost = BigDecimal.ZERO;
BigDecimal totalInsurance = BigDecimal.ZERO;
int totalLos = 0;
for (MrDrgGrouping g : list) {
if (g.getTotalCost() != null) totalCost = totalCost.add(g.getTotalCost());
if (g.getInsurancePayment() != null) totalInsurance = totalInsurance.add(g.getInsurancePayment());
if (g.getLosDays() != null) totalLos += g.getLosDays();
}
int count = list.size();
result.put("totalCost", totalCost);
result.put("totalInsurance", totalInsurance);
result.put("avgCost", count > 0 ? totalCost.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
result.put("avgLos", count > 0 ? BigDecimal.valueOf(totalLos).divide(BigDecimal.valueOf(count), 1, RoundingMode.HALF_UP) : BigDecimal.ZERO);
result.put("insuranceRate", totalCost.compareTo(BigDecimal.ZERO) > 0
? totalInsurance.multiply(BigDecimal.valueOf(100)).divide(totalCost, 2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
Map<String, Integer> typeCount = list.stream()
.collect(Collectors.groupingBy(
g -> g.getGroupingType() != null ? g.getGroupingType() : "UNKNOWN",
Collectors.summingInt(g -> 1)));
result.put("typeDistribution", typeCount);
Map<String, BigDecimal> drgCostMap = new LinkedHashMap<>();
Map<String, Integer> drgCountMap = new LinkedHashMap<>();
for (MrDrgGrouping g : list) {
String code = g.getDrgCode();
if (StringUtils.hasText(code)) {
drgCostMap.merge(code, g.getTotalCost() != null ? g.getTotalCost() : BigDecimal.ZERO, BigDecimal::add);
drgCountMap.merge(code, 1, Integer::sum);
}
}
List<Map<String, Object>> topDrg = drgCostMap.entrySet().stream()
.sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
.limit(10)
.map(e -> {
Map<String, Object> item = new HashMap<>();
item.put("drgCode", e.getKey());
item.put("count", drgCountMap.getOrDefault(e.getKey(), 0));
item.put("totalCost", e.getValue());
return item;
})
.collect(Collectors.toList());
result.put("topDrgByCost", topDrg);
return R.ok(result);
}
@Override
public R<?> getCostAlert(Map<String, Object> params) {
String alertLevel = (String) params.get("alertLevel");
String handleStatus = (String) params.get("handleStatus");
LambdaQueryWrapper<DrgCostAlert> w = new LambdaQueryWrapper<>();
if (StringUtils.hasText(alertLevel)) {
w.eq(DrgCostAlert::getAlertLevel, alertLevel);
}
if (StringUtils.hasText(handleStatus)) {
w.eq(DrgCostAlert::getHandleStatus, handleStatus);
}
w.orderByDesc(DrgCostAlert::getCreateTime);
List<DrgCostAlert> list = drgCostAlertService.list(w);
Map<String, Object> result = new HashMap<>();
result.put("total", list.size());
long unhandled = list.stream().filter(a -> "PENDING".equals(a.getHandleStatus())).count();
long highLevel = list.stream().filter(a -> "HIGH".equals(a.getAlertLevel())).count();
result.put("unhandledCount", unhandled);
result.put("highLevelCount", highLevel);
result.put("records", list);
return R.ok(result);
}
@Override
public R<?> getOptimization(Map<String, Object> params) {
LambdaQueryWrapper<DrgPerformance> w = new LambdaQueryWrapper<>();
w.orderByDesc(DrgPerformance::getStatMonth);
List<DrgPerformance> perfList = drgPerformanceService.list(w);
Map<String, Object> result = new HashMap<>();
if (!perfList.isEmpty()) {
DrgPerformance latest = perfList.get(0);
result.put("latestMonth", latest.getStatMonth());
result.put("totalCases", latest.getTotalCases());
result.put("drgCoveredRate", latest.getDrgCoveredRate());
result.put("avgWeight", latest.getAvgWeight());
result.put("avgCost", latest.getAvgCost());
result.put("costControlRate", latest.getCostControlRate());
result.put("cmiValue", latest.getCmiValue());
result.put("avgLos", latest.getAvgLos());
}
List<Map<String, Object>> trend = new ArrayList<>();
for (DrgPerformance p : perfList) {
Map<String, Object> item = new HashMap<>();
item.put("month", p.getStatMonth());
item.put("totalCases", p.getTotalCases());
item.put("avgWeight", p.getAvgWeight());
item.put("avgCost", p.getAvgCost());
item.put("costControlRate", p.getCostControlRate());
item.put("cmiValue", p.getCmiValue());
trend.add(item);
}
result.put("trend", trend);
return R.ok(result);
}
}

View File

@@ -0,0 +1,41 @@
package com.healthlink.his.web.ybmanage.controller;
import com.core.common.core.domain.AjaxResult;
import com.healthlink.his.web.ybmanage.appservice.IDrgDeepAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Tag(name = "DRG/DIP深化分析")
@RestController
@RequestMapping("/ybmanage/drg-deep")
public class DrgDeepController {
@Autowired
private IDrgDeepAppService drgDeepAppService;
@Operation(summary = "DRG分组分析")
@PreAuthorize("@ss.hasPermi('ybmanage:drgdeep:edit')")
@PostMapping("/analyze")
public AjaxResult analyzeDrg(@RequestBody Map<String, Object> params) {
return AjaxResult.success(drgDeepAppService.analyzeDrg(params).getData());
}
@Operation(summary = "费用预警查询")
@PreAuthorize("@ss.hasPermi('ybmanage:drgdeep:list')")
@PostMapping("/cost-alert")
public AjaxResult getCostAlert(@RequestBody Map<String, Object> params) {
return AjaxResult.success(drgDeepAppService.getCostAlert(params).getData());
}
@Operation(summary = "优化建议")
@PreAuthorize("@ss.hasPermi('ybmanage:drgdeep:list')")
@PostMapping("/optimization")
public AjaxResult getOptimization(@RequestBody Map<String, Object> params) {
return AjaxResult.success(drgDeepAppService.getOptimization(params).getData());
}
}

View File

@@ -0,0 +1,199 @@
<template>
<div style="padding: 16px">
<div style="margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center">
<span style="font-size: 18px; font-weight: bold">DRG/DIP深化分析</span>
<el-button type="primary" @click="refreshAll">刷新</el-button>
</div>
<el-row :gutter="16" style="margin-bottom: 16px">
<el-col :span="6">
<el-card shadow="hover" :body-style="{ padding: '12px' }">
<div style="text-align: center">
<div style="font-size: 22px; font-weight: bold; color: #409eff">{{ deepStats.totalCases || 0 }}</div>
<div style="font-size: 12px; color: #999">分析病例数</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{ padding: '12px' }">
<div style="text-align: center">
<div style="font-size: 22px; font-weight: bold; color: #67c23a">{{ formatMoney(deepStats.avgCost) }}</div>
<div style="font-size: 12px; color: #999">平均费用()</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{ padding: '12px' }">
<div style="text-align: center">
<div style="font-size: 22px; font-weight: bold; color: #e6a23c">{{ deepStats.avgLos || 0 }}</div>
<div style="font-size: 12px; color: #999">平均住院日</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{ padding: '12px' }">
<div style="text-align: center">
<div style="font-size: 22px; font-weight: bold; color: #f56c6c">{{ deepStats.insuranceRate || 0 }}%</div>
<div style="font-size: 12px; color: #999">医保支付率</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="16" style="margin-bottom: 16px">
<el-col :span="12">
<el-card shadow="never">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center">
<span>费用预警</span>
<span style="font-size: 12px; color: #f56c6c">未处理: {{ costAlert.unhandledCount || 0 }} / 高危: {{ costAlert.highLevelCount || 0 }}</span>
</div>
</template>
<el-table :data="(costAlert.records || []).slice(0, 5)" border stripe size="small">
<el-table-column prop="patientName" label="患者" width="100" />
<el-table-column prop="drgCode" label="DRG组" width="100" />
<el-table-column prop="alertLevel" label="等级" width="80">
<template #default="{ row }">
<el-tag :type="row.alertLevel === 'HIGH' ? 'danger' : row.alertLevel === 'MEDIUM' ? 'warning' : 'info'" size="small">{{ row.alertLevel }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="handleStatus" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.handleStatus === 'PENDING' ? 'danger' : 'success'" size="small">{{ row.handleStatus === 'PENDING' ? '待处理' : '已处理' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="costDeviation" label="偏差" align="right">
<template #default="{ row }">{{ formatMoney(row.costDeviation) }}</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header>优化趋势</template>
<el-table :data="(optimization.trend || []).slice(0, 5)" border stripe size="small">
<el-table-column prop="month" label="月份" width="100" />
<el-table-column prop="totalCases" label="病例数" width="80" align="center" />
<el-table-column prop="avgWeight" label="平均权重" width="100" align="right" />
<el-table-column prop="avgCost" label="平均费用" align="right">
<template #default="{ row }">{{ formatMoney(row.avgCost) }}</template>
</el-table-column>
<el-table-column prop="costControlRate" label="费用控制率" width="100" align="right">
<template #default="{ row }">{{ row.costControlRate }}%</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<el-card shadow="never" style="margin-bottom: 16px">
<template #header>DRG分组分析</template>
<div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 12px">
<el-select v-model="analyzeQuery.groupingType" placeholder="分组类型" clearable style="width: 120px">
<el-option label="DRG" value="DRG" />
<el-option label="DIP" value="DIP" />
</el-select>
<el-date-picker v-model="analyzeQuery.dateRange" type="daterange" start-placeholder="开始" end-placeholder="结束" style="width: 260px" />
<el-button type="primary" @click="runAnalysis">分析</el-button>
</div>
<el-table :data="deepStats.topDrgByCost || []" border stripe>
<el-table-column type="index" label="排名" width="60" align="center" />
<el-table-column prop="drgCode" label="DRG组代码" width="140" />
<el-table-column prop="count" label="病例数" width="100" align="center" />
<el-table-column prop="totalCost" label="总费用(万元)" align="right">
<template #default="{ row }">{{ formatMoney(row.totalCost) }}</template>
</el-table-column>
</el-table>
</el-card>
<el-card shadow="never">
<template #header>当前绩效指标</template>
<el-row :gutter="16">
<el-col :span="4">
<div style="text-align: center">
<div style="font-size: 20px; font-weight: bold; color: #409eff">{{ optimization.totalCases || 0 }}</div>
<div style="font-size: 12px; color: #999">当月病例数</div>
</div>
</el-col>
<el-col :span="4">
<div style="text-align: center">
<div style="font-size: 20px; font-weight: bold; color: #67c23a">{{ optimization.drgCoveredRate || 0 }}%</div>
<div style="font-size: 12px; color: #999">DRG入组率</div>
</div>
</el-col>
<el-col :span="4">
<div style="text-align: center">
<div style="font-size: 20px; font-weight: bold; color: #e6a23c">{{ optimization.avgWeight || 0 }}</div>
<div style="font-size: 12px; color: #999">平均权重</div>
</div>
</el-col>
<el-col :span="4">
<div style="text-align: center">
<div style="font-size: 20px; font-weight: bold; color: #f56c6c">{{ formatMoney(optimization.avgCost) }}</div>
<div style="font-size: 12px; color: #999">平均费用()</div>
</div>
</el-col>
<el-col :span="4">
<div style="text-align: center">
<div style="font-size: 20px; font-weight: bold; color: #409eff">{{ optimization.costControlRate || 0 }}%</div>
<div style="font-size: 12px; color: #999">费用控制率</div>
</div>
</el-col>
<el-col :span="4">
<div style="text-align: center">
<div style="font-size: 20px; font-weight: bold; color: #67c23a">{{ optimization.cmiValue || 0 }}</div>
<div style="font-size: 12px; color: #999">CMI值</div>
</div>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { analyzeDrgDeep, getCostAlert, getOptimization } from './deepApi'
const deepStats = ref({})
const costAlert = ref({})
const optimization = ref({})
const analyzeQuery = ref({ groupingType: '', dateRange: null })
function formatMoney(val) {
if (!val) return '0.00'
return (Number(val) / 10000).toFixed(2)
}
async function refreshAll() {
try {
const [a, c, o] = await Promise.all([
analyzeDrgDeep({}),
getCostAlert({}),
getOptimization({})
])
deepStats.value = a.data || {}
costAlert.value = c.data || {}
optimization.value = o.data || {}
} catch (e) {
ElMessage.error('加载数据失败')
}
}
async function runAnalysis() {
const params = { groupingType: analyzeQuery.value.groupingType }
if (analyzeQuery.value.dateRange && analyzeQuery.value.dateRange.length === 2) {
params.startDate = analyzeQuery.value.dateRange[0]
params.endDate = analyzeQuery.value.dateRange[1]
}
try {
const r = await analyzeDrgDeep(params)
deepStats.value = r.data || {}
ElMessage.success('分析完成')
} catch (e) {
ElMessage.error('分析失败')
}
}
onMounted(() => refreshAll())
</script>

View File

@@ -0,0 +1,13 @@
import request from '@/utils/request'
export function analyzeDrgDeep(data) {
return request({ url: '/ybmanage/drg-deep/analyze', method: 'post', data })
}
export function getCostAlert(data) {
return request({ url: '/ybmanage/drg-deep/cost-alert', method: 'post', data })
}
export function getOptimization(data) {
return request({ url: '/ybmanage/drg-deep/optimization', method: 'post', data })
}