feat(ybmanage): add DRG/DIP deep analysis module (T13.5)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
199
healthlink-his-ui/src/views/drganalysis/DrgDeepAnalysis.vue
Normal file
199
healthlink-his-ui/src/views/drganalysis/DrgDeepAnalysis.vue
Normal 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>
|
||||
13
healthlink-his-ui/src/views/drganalysis/deepApi.js
Normal file
13
healthlink-his-ui/src/views/drganalysis/deepApi.js
Normal 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 })
|
||||
}
|
||||
Reference in New Issue
Block a user