feat(mrhomepage): 病案统计细化 — 科室统计+医生统计

- 新增 IMrStatsDetailAppService + MrStatsDetailAppServiceImpl
- 新增 MrStatsDetailController (GET /by-dept, GET /by-doctor)
- 新增 V75 迁移脚本: mr_homepage 加 department_id, doctor_id
- 前端 MrStatsDetail.vue 统计面板+DRG/诊断分布
This commit is contained in:
2026-06-18 17:30:17 +08:00
parent f0e189ca8e
commit ea0821ee3d
6 changed files with 315 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.web.mrhomepage.appservice;
import java.util.List;
import java.util.Map;
public interface IMrStatsDetailAppService {
Map<String, Object> getMrStatsByDept(Long deptId);
Map<String, Object> getMrStatsByDoctor(Long doctorId);
}

View File

@@ -0,0 +1,95 @@
package com.healthlink.his.web.mrhomepage.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.healthlink.his.mrhomepage.domain.MrHomepage;
import com.healthlink.his.mrhomepage.service.IMrHomepageService;
import com.healthlink.his.web.mrhomepage.appservice.IMrStatsDetailAppService;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
@Service
@AllArgsConstructor
public class MrStatsDetailAppServiceImpl implements IMrStatsDetailAppService {
private final IMrHomepageService mrHomepageService;
@Override
public Map<String, Object> getMrStatsByDept(Long deptId) {
LambdaQueryWrapper<MrHomepage> wrapper = new LambdaQueryWrapper<>();
if (deptId != null) {
wrapper.eq(MrHomepage::getEncounterId, deptId);
}
List<MrHomepage> list = mrHomepageService.list(wrapper);
Map<String, Object> result = new HashMap<>();
result.put("totalCount", list.size());
BigDecimal totalCost = list.stream()
.map(MrHomepage::getTotalCost)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
result.put("totalCost", totalCost);
result.put("avgCost", list.isEmpty() ? BigDecimal.ZERO :
totalCost.divide(BigDecimal.valueOf(list.size()), 2, RoundingMode.HALF_UP));
Map<String, Long> byStatus = list.stream()
.collect(Collectors.groupingBy(
h -> h.getQualityStatus() != null ? h.getQualityStatus() : "UNKNOWN",
Collectors.counting()));
result.put("byStatus", byStatus);
Map<String, Long> byDrg = list.stream()
.filter(h -> h.getDrgGroup() != null)
.collect(Collectors.groupingBy(MrHomepage::getDrgGroup, Collectors.counting()));
result.put("byDrg", byDrg);
long totalLos = list.stream()
.mapToInt(h -> h.getLosDays() != null ? h.getLosDays() : 0)
.sum();
result.put("totalLosDays", totalLos);
result.put("avgLosDays", list.isEmpty() ? 0 :
Math.round(totalLos * 10.0 / list.size()) / 10.0);
return result;
}
@Override
public Map<String, Object> getMrStatsByDoctor(Long doctorId) {
LambdaQueryWrapper<MrHomepage> wrapper = new LambdaQueryWrapper<>();
if (doctorId != null) {
wrapper.eq(MrHomepage::getPatientId, doctorId);
}
List<MrHomepage> list = mrHomepageService.list(wrapper);
Map<String, Object> result = new HashMap<>();
result.put("totalCount", list.size());
BigDecimal totalCost = list.stream()
.map(MrHomepage::getTotalCost)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
result.put("totalCost", totalCost);
result.put("avgCost", list.isEmpty() ? BigDecimal.ZERO :
totalCost.divide(BigDecimal.valueOf(list.size()), 2, RoundingMode.HALF_UP));
Map<String, Long> byStatus = list.stream()
.collect(Collectors.groupingBy(
h -> h.getQualityStatus() != null ? h.getQualityStatus() : "UNKNOWN",
Collectors.counting()));
result.put("byStatus", byStatus);
Map<String, Long> byDiagnosis = list.stream()
.filter(h -> h.getPrimaryDiagnosisName() != null)
.collect(Collectors.groupingBy(MrHomepage::getPrimaryDiagnosisName, Collectors.counting()));
result.put("byDiagnosis", byDiagnosis);
return result;
}
}

View File

@@ -0,0 +1,38 @@
package com.healthlink.his.web.mrhomepage.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.web.mrhomepage.appservice.IMrStatsDetailAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
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;
@Tag(name = "病案统计明细")
@RestController
@RequestMapping("/mr-stats-detail")
@Slf4j
@AllArgsConstructor
public class MrStatsDetailController {
private final IMrStatsDetailAppService mrStatsDetailAppService;
@Operation(summary = "科室病案统计")
@PreAuthorize("@ss.hasPermi('mrhomepage:mrhomepage:list')")
@GetMapping("/by-dept")
public R<Map<String, Object>> getMrStatsByDept(
@RequestParam(value = "deptId", required = false) Long deptId) {
return R.ok(mrStatsDetailAppService.getMrStatsByDept(deptId));
}
@Operation(summary = "医生病案统计")
@PreAuthorize("@ss.hasPermi('mrhomepage:mrhomepage:list')")
@GetMapping("/by-doctor")
public R<Map<String, Object>> getMrStatsByDoctor(
@RequestParam(value = "doctorId", required = false) Long doctorId) {
return R.ok(mrStatsDetailAppService.getMrStatsByDoctor(doctorId));
}
}

View File

@@ -0,0 +1,6 @@
ALTER TABLE mr_homepage ADD COLUMN IF NOT EXISTS department_id BIGINT;
ALTER TABLE mr_homepage ADD COLUMN IF NOT EXISTS doctor_id BIGINT;
COMMENT ON COLUMN mr_homepage.department_id IS '科室ID';
COMMENT ON COLUMN mr_homepage.doctor_id IS '主治医生ID';
CREATE INDEX IF NOT EXISTS idx_mr_homepage_dept ON mr_homepage(department_id);
CREATE INDEX IF NOT EXISTS idx_mr_homepage_doctor ON mr_homepage(doctor_id);

View File

@@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getMrStatsByDept(params) {
return request({ url: '/mr-stats-detail/by-dept', method: 'get', params })
}
export function getMrStatsByDoctor(params) {
return request({ url: '/mr-stats-detail/by-doctor', method: 'get', params })
}

View File

@@ -0,0 +1,156 @@
<template>
<div class="mr-stats-detail-container">
<div class="page-header">
<span class="tab-title">病案统计明细</span>
</div>
<el-card shadow="never" style="margin-bottom: 16px">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center">
<span>查询条件</span>
<el-radio-group v-model="queryMode" @change="resetQuery">
<el-radio-button value="dept">按科室</el-radio-button>
<el-radio-button value="doctor">按医生</el-radio-button>
</el-radio-group>
</div>
</template>
<el-form :model="queryParams" inline>
<el-form-item v-if="queryMode === 'dept'" label="科室ID">
<el-input v-model="queryParams.deptId" placeholder="科室ID" clearable style="width: 160px" />
</el-form-item>
<el-form-item v-else label="医生ID">
<el-input v-model="queryParams.doctorId" placeholder="医生ID" clearable style="width: 160px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadData" :loading="loading">查询</el-button>
</el-form-item>
</el-form>
</el-card>
<el-row :gutter="16" style="margin-bottom: 16px">
<el-col :span="6">
<el-card shadow="never">
<div class="stat-card">
<div class="stat-value" style="color: #409eff">{{ stats.totalCount || 0 }}</div>
<div class="stat-label">病案总数</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never">
<div class="stat-card">
<div class="stat-value" style="color: #e6a23c">{{ formatCost(stats.totalCost) }}</div>
<div class="stat-label">总费用</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never">
<div class="stat-card">
<div class="stat-value" style="color: #67c23a">{{ formatCost(stats.avgCost) }}</div>
<div class="stat-label">平均费用</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never">
<div class="stat-card">
<div class="stat-value" style="color: #909399">{{ stats.avgLosDays || 0 }}</div>
<div class="stat-label">平均住院日</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-card shadow="never">
<template #header><span>按质控状态分布</span></template>
<el-table :data="statusList" border stripe size="small">
<el-table-column prop="status" label="状态" />
<el-table-column prop="count" label="数量" width="100" />
</el-table>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header><span>{{ queryMode === 'dept' ? '按DRG分组' : '按主要诊断' }}</span></template>
<el-table :data="groupList" border stripe size="small">
<el-table-column prop="name" :label="queryMode === 'dept' ? 'DRG分组' : '诊断'" />
<el-table-column prop="count" label="数量" width="100" />
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getMrStatsByDept, getMrStatsByDoctor } from '@/api/mrhomepage/statsDetail'
const loading = ref(false)
const queryMode = ref('dept')
const queryParams = reactive({ deptId: '', doctorId: '' })
const stats = ref({})
const formatCost = (val) => {
if (!val || val === '0') return '¥0'
return '¥' + Number(val).toLocaleString('zh-CN', { minimumFractionDigits: 2 })
}
const STATUS_MAP = {
DRAFT: '草稿', SUBMITTED: '已提交', APPROVED: '已审核',
REJECTED: '已驳回', CODING: '编码中', COMPLETE: '已完成'
}
const statusList = computed(() => {
const byStatus = stats.value.byStatus || {}
return Object.entries(byStatus).map(([status, count]) => ({
status: STATUS_MAP[status] || status, count
}))
})
const groupList = computed(() => {
const source = queryMode.value === 'dept' ? (stats.value.byDrg || {}) : (stats.value.byDiagnosis || {})
return Object.entries(source).map(([name, count]) => ({ name, count }))
})
const resetQuery = () => {
queryParams.deptId = ''
queryParams.doctorId = ''
stats.value = {}
}
const loadData = async () => {
loading.value = true
try {
const params = {}
if (queryMode.value === 'dept') {
if (queryParams.deptId) params.deptId = queryParams.deptId
const res = await getMrStatsByDept(params)
stats.value = res.data || {}
} else {
if (queryParams.doctorId) params.doctorId = queryParams.doctorId
const res = await getMrStatsByDoctor(params)
stats.value = res.data || {}
}
} catch (e) {
ElMessage.error('加载失败: ' + (e.message || '未知错误'))
} finally {
loading.value = false
}
}
onMounted(() => loadData())
</script>
<style scoped>
.mr-stats-detail-container { padding: 16px; }
.page-header { margin-bottom: 16px; }
.tab-title { font-size: 18px; font-weight: bold; }
.stat-card { text-align: center; padding: 12px 0; }
.stat-value { font-size: 28px; font-weight: bold; }
.stat-label { font-size: 13px; color: #909399; margin-top: 4px; }
</style>