feat(reportmanage): T11.1 DRG/DIP分析模块 - AppService + Controller + Frontend
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
package com.healthlink.his.web.reportmanage.appservice;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IDrgAnalysisAppService {
|
||||
R<?> analyzeDrg(Map<String, Object> params);
|
||||
R<?> getDrgStats(String startDate, String endDate);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.healthlink.his.web.reportmanage.appservice.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.crossmodule.domain.DrgPerformance;
|
||||
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.reportmanage.appservice.IDrgAnalysisAppService;
|
||||
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 DrgAnalysisAppServiceImpl implements IDrgAnalysisAppService {
|
||||
|
||||
private final IMrDrgGroupingService drgGroupingService;
|
||||
private final IDrgPerformanceService drgPerformanceService;
|
||||
|
||||
@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<?> getDrgStats(String startDate, String endDate) {
|
||||
LambdaQueryWrapper<DrgPerformance> w = new LambdaQueryWrapper<>();
|
||||
if (StringUtils.hasText(startDate)) {
|
||||
w.ge(DrgPerformance::getStatMonth, startDate);
|
||||
}
|
||||
if (StringUtils.hasText(endDate)) {
|
||||
w.le(DrgPerformance::getStatMonth, endDate);
|
||||
}
|
||||
w.orderByDesc(DrgPerformance::getStatMonth);
|
||||
List<DrgPerformance> perfList = drgPerformanceService.list(w);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("totalRecords", perfList.size());
|
||||
|
||||
if (!perfList.isEmpty()) {
|
||||
DrgPerformance latest = perfList.get(0);
|
||||
result.put("latestMonth", latest.getStatMonth());
|
||||
result.put("latestTotalCases", latest.getTotalCases());
|
||||
result.put("latestDrgCoveredRate", latest.getDrgCoveredRate());
|
||||
result.put("latestAvgWeight", latest.getAvgWeight());
|
||||
result.put("latestAvgCost", latest.getAvgCost());
|
||||
result.put("latestCostControlRate", latest.getCostControlRate());
|
||||
result.put("latestCmiValue", latest.getCmiValue());
|
||||
}
|
||||
|
||||
BigDecimal totalCases = BigDecimal.ZERO;
|
||||
BigDecimal totalWeight = BigDecimal.ZERO;
|
||||
BigDecimal totalCostControl = BigDecimal.ZERO;
|
||||
for (DrgPerformance p : perfList) {
|
||||
if (p.getTotalCases() != null) totalCases = totalCases.add(BigDecimal.valueOf(p.getTotalCases()));
|
||||
if (p.getAvgWeight() != null) totalWeight = totalWeight.add(p.getAvgWeight());
|
||||
if (p.getCostControlRate() != null) totalCostControl = totalCostControl.add(p.getCostControlRate());
|
||||
}
|
||||
int size = perfList.size();
|
||||
result.put("periodTotalCases", totalCases);
|
||||
result.put("avgWeight", size > 0 ? totalWeight.divide(BigDecimal.valueOf(size), 4, RoundingMode.HALF_UP) : BigDecimal.ZERO);
|
||||
result.put("avgCostControlRate", size > 0 ? totalCostControl.divide(BigDecimal.valueOf(size), 2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
|
||||
|
||||
return R.ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.healthlink.his.web.reportmanage.controller;
|
||||
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.web.reportmanage.appservice.IDrgAnalysisAppService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/report/drg")
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class DrgAnalysisController {
|
||||
|
||||
private final IDrgAnalysisAppService drgAnalysisAppService;
|
||||
|
||||
@PostMapping("/analyze")
|
||||
@PreAuthorize("hasAuthority('infection:report:edit')")
|
||||
public R<?> analyzeDrg(@RequestBody Map<String, Object> params) {
|
||||
return drgAnalysisAppService.analyzeDrg(params);
|
||||
}
|
||||
|
||||
@GetMapping("/stats")
|
||||
@PreAuthorize("hasAuthority('infection:report:list')")
|
||||
public R<?> getDrgStats(
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate) {
|
||||
return drgAnalysisAppService.getDrgStats(startDate, endDate);
|
||||
}
|
||||
}
|
||||
3
healthlink-his-ui/src/views/drganalysis/api.js
Normal file
3
healthlink-his-ui/src/views/drganalysis/api.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import request from '@/utils/request'
|
||||
export function analyzeDrg(d){return request({url:'/report/drg/analyze',method:'post',data:d})}
|
||||
export function getDrgStats(p){return request({url:'/report/drg/stats',method:'get',params:p})}
|
||||
191
healthlink-his-ui/src/views/drganalysis/index.vue
Normal file
191
healthlink-his-ui/src/views/drganalysis/index.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<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>
|
||||
<div>
|
||||
<el-button type="primary" @click="loadStats">刷新</el-button>
|
||||
<el-button type="success" @click="runAnalysis">执行分析</el-button>
|
||||
</div>
|
||||
</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">{{ stats.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(stats.totalCost) }}</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">{{ formatMoney(stats.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:#f56c6c">{{ stats.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="6">
|
||||
<el-card shadow="hover" :body-style="{padding:'12px'}">
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:22px;font-weight:bold;color:#409eff">{{ stats.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:#67c23a">{{ formatMoney(stats.totalInsurance) }}</div>
|
||||
<div style="font-size:12px;color:#999">医保总支付(万)</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" :body-style="{padding:'12px'}">
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:14px;font-weight:bold;color:#909399;margin-bottom:8px">分组类型分布</div>
|
||||
<div v-for="(v,k) in (stats.typeDistribution || {})" :key="k" style="display:inline-block;margin:0 12px">
|
||||
<span style="font-size:18px;font-weight:bold;color:#409eff">{{ v }}</span>
|
||||
<span style="font-size:12px;color:#666"> {{ k }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="never" style="margin-bottom:16px">
|
||||
<template #header>DRG绩效指标</template>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="4">
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:20px;font-weight:bold;color:#409eff">{{ perfStats.latestTotalCases || 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">{{ perfStats.latestDrgCoveredRate || 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">{{ perfStats.latestAvgWeight || 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(perfStats.latestAvgCost) }}</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">{{ perfStats.latestCostControlRate || 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">{{ perfStats.latestCmiValue || 0 }}</div>
|
||||
<div style="font-size:12px;color:#999">CMI值</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" style="margin-bottom:16px">
|
||||
<template #header>费用TOP10 DRG组</template>
|
||||
<el-table :data="stats.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>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
||||
<el-select v-model="query.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="query.dateRange" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" style="width:260px" />
|
||||
<el-button type="primary" @click="runAnalysis">分析</el-button>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted} from 'vue'
|
||||
import {ElMessage} from 'element-plus'
|
||||
import {analyzeDrg, getDrgStats} from './api'
|
||||
|
||||
const loading = ref(false)
|
||||
const stats = ref({})
|
||||
const perfStats = ref({})
|
||||
const query = ref({groupingType:'', dateRange:null})
|
||||
|
||||
function formatMoney(val) {
|
||||
if (!val) return '0.00'
|
||||
return (val / 10000).toFixed(2)
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [s, p] = await Promise.all([getDrgStats({}), getDrgStats({})])
|
||||
stats.value = s.data || {}
|
||||
perfStats.value = p.data || {}
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function runAnalysis() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {groupingType: query.value.groupingType}
|
||||
if (query.value.dateRange && query.value.dateRange.length === 2) {
|
||||
params.startDate = query.value.dateRange[0]
|
||||
params.endDate = query.value.dateRange[1]
|
||||
}
|
||||
const r = await analyzeDrg(params)
|
||||
stats.value = r.data || {}
|
||||
const p = await getDrgStats(params)
|
||||
perfStats.value = p.data || {}
|
||||
ElMessage.success('分析完成')
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
function resetQuery() {
|
||||
query.value = {groupingType:'', dateRange:null}
|
||||
loadStats()
|
||||
}
|
||||
|
||||
onMounted(() => loadStats())
|
||||
</script>
|
||||
Reference in New Issue
Block a user