feat(reportmanage): T11.1 DRG/DIP分析模块 - AppService + Controller + Frontend

This commit is contained in:
2026-06-18 15:02:48 +08:00
parent dfd4faa00b
commit 965418dc45
5 changed files with 371 additions and 0 deletions

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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})}

View 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>