feat(P3): 病案管理完善 — DRG/DIP分组+统计分析
- MrDrgController: DRG/DIP分组/无效标记/统计/排名 - MrDrgGrouping: 分组结果实体+V28 Flyway迁移 - 前端drg: 分组列表+DRG排名+统计卡片 - 后端编译通过,前端构建通过
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
package com.healthlink.his.web.mrhomepage.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.core.common.core.domain.R;
|
||||
import com.healthlink.his.mrhomepage.domain.MrDrgGrouping;
|
||||
import com.healthlink.his.mrhomepage.service.IMrDrgGroupingService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* DRG/DIP分组 + 病案归档统计 Controller
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/mr-drg")
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class MrDrgController {
|
||||
|
||||
private final IMrDrgGroupingService drgGroupingService;
|
||||
|
||||
// ==================== DRG/DIP分组 ====================
|
||||
|
||||
@GetMapping("/page")
|
||||
public R<?> getPage(
|
||||
@RequestParam(value = "groupingType", required = false) String groupingType,
|
||||
@RequestParam(value = "drgCode", required = false) String drgCode,
|
||||
@RequestParam(value = "patientName", required = false) String patientName,
|
||||
@RequestParam(value = "isValid", required = false) Boolean isValid,
|
||||
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
|
||||
LambdaQueryWrapper<MrDrgGrouping> w = new LambdaQueryWrapper<>();
|
||||
w.eq(StringUtils.hasText(groupingType), MrDrgGrouping::getGroupingType, groupingType)
|
||||
.like(StringUtils.hasText(drgCode), MrDrgGrouping::getDrgCode, drgCode)
|
||||
.like(StringUtils.hasText(patientName), MrDrgGrouping::getPatientName, patientName)
|
||||
.eq(isValid != null, MrDrgGrouping::getIsValid, isValid)
|
||||
.orderByDesc(MrDrgGrouping::getDischargeDate);
|
||||
return R.ok(drgGroupingService.page(new Page<>(pageNo, pageSize), w));
|
||||
}
|
||||
|
||||
@PostMapping("/group")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> groupDrg(@RequestBody MrDrgGrouping grouping) {
|
||||
// 简化DRG分组逻辑 — 根据主诊断+主手术推算DRG组
|
||||
String drgCode = calculateDrgCode(grouping);
|
||||
grouping.setDrgCode(drgCode);
|
||||
grouping.setDrgName(getDrgName(drgCode));
|
||||
grouping.setDrgWeight(getDrgWeight(drgCode));
|
||||
grouping.setIsValid(true);
|
||||
grouping.setCreateTime(new Date());
|
||||
drgGroupingService.save(grouping);
|
||||
return R.ok(grouping);
|
||||
}
|
||||
|
||||
@PutMapping("/invalidate/{id}")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public R<?> invalidate(@PathVariable Long id, @RequestParam("reason") String reason) {
|
||||
MrDrgGrouping g = drgGroupingService.getById(id);
|
||||
if (g == null) return R.fail("分组记录不存在");
|
||||
g.setIsValid(false);
|
||||
g.setInvalidReason(reason);
|
||||
drgGroupingService.updateById(g);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
// ==================== 统计分析 ====================
|
||||
|
||||
@GetMapping("/stats/overview")
|
||||
public R<?> getOverview(
|
||||
@RequestParam(value = "startDate", required = false) String startDate,
|
||||
@RequestParam(value = "endDate", required = false) String endDate) {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
// DRG分组统计
|
||||
LambdaQueryWrapper<MrDrgGrouping> drgW = new LambdaQueryWrapper<>();
|
||||
drgW.eq(MrDrgGrouping::getGroupingType, "DRG");
|
||||
stats.put("drgTotal", drgGroupingService.count(drgW));
|
||||
|
||||
// DIP分组统计
|
||||
LambdaQueryWrapper<MrDrgGrouping> dipW = new LambdaQueryWrapper<>();
|
||||
dipW.eq(MrDrgGrouping::getGroupingType, "DIP");
|
||||
stats.put("dipTotal", drgGroupingService.count(dipW));
|
||||
|
||||
// 无效分组数
|
||||
LambdaQueryWrapper<MrDrgGrouping> invW = new LambdaQueryWrapper<>();
|
||||
invW.eq(MrDrgGrouping::getIsValid, false);
|
||||
stats.put("invalidCount", drgGroupingService.count(invW));
|
||||
|
||||
// 费用统计
|
||||
List<MrDrgGrouping> all = drgGroupingService.list();
|
||||
BigDecimal totalCost = BigDecimal.ZERO;
|
||||
BigDecimal totalInsurance = BigDecimal.ZERO;
|
||||
for (MrDrgGrouping g : all) {
|
||||
if (g.getTotalCost() != null) totalCost = totalCost.add(g.getTotalCost());
|
||||
if (g.getInsurancePayment() != null) totalInsurance = totalInsurance.add(g.getInsurancePayment());
|
||||
}
|
||||
stats.put("totalCost", totalCost);
|
||||
stats.put("totalInsurance", totalInsurance);
|
||||
|
||||
return R.ok(stats);
|
||||
}
|
||||
|
||||
@GetMapping("/stats/top-drg")
|
||||
public R<?> getTopDrg(@RequestParam(value = "limit", defaultValue = "10") Integer limit) {
|
||||
List<MrDrgGrouping> all = drgGroupingService.list();
|
||||
Map<String, Integer> drgCount = new LinkedHashMap<>();
|
||||
Map<String, BigDecimal> drgCost = new LinkedHashMap<>();
|
||||
for (MrDrgGrouping g : all) {
|
||||
String code = g.getDrgCode();
|
||||
if (code != null) {
|
||||
drgCount.merge(code, 1, Integer::sum);
|
||||
drgCost.merge(code, g.getTotalCost() != null ? g.getTotalCost() : BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
}
|
||||
List<Map<String, Object>> topList = new ArrayList<>();
|
||||
drgCount.entrySet().stream()
|
||||
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
|
||||
.limit(limit)
|
||||
.forEach(e -> {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("drgCode", e.getKey());
|
||||
item.put("count", e.getValue());
|
||||
item.put("totalCost", drgCost.getOrDefault(e.getKey(), BigDecimal.ZERO));
|
||||
topList.add(item);
|
||||
});
|
||||
return R.ok(topList);
|
||||
}
|
||||
|
||||
// ==================== DRG分组算法(简化版) ====================
|
||||
|
||||
private String calculateDrgCode(MrDrgGrouping g) {
|
||||
// 简化DRG分组: 根据主诊断前3位+主手术确定DRG组
|
||||
String diagCode = g.getPrimaryDiagnosisCode();
|
||||
String procCode = g.getPrimaryProcedureCode();
|
||||
if (diagCode == null || diagCode.isEmpty()) return "ZZ99";
|
||||
String mdc = diagCode.substring(0, Math.min(3, diagCode.length()));
|
||||
if (procCode != null && !procCode.isEmpty()) {
|
||||
return mdc + "-" + procCode.substring(0, Math.min(2, procCode.length()));
|
||||
}
|
||||
return mdc + "-MED";
|
||||
}
|
||||
|
||||
private String getDrgName(String code) {
|
||||
return "DRG组(" + code + ")";
|
||||
}
|
||||
|
||||
private BigDecimal getDrgWeight(String code) {
|
||||
// 简化权重计算
|
||||
return new BigDecimal("1.0000");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
-- V28: 病案管理增强 — DRG/DIP分组+归档统计
|
||||
|
||||
-- DRG/DIP分组结果表
|
||||
CREATE TABLE IF NOT EXISTS mr_drg_grouping (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
encounter_id BIGINT NOT NULL,
|
||||
patient_id BIGINT NOT NULL,
|
||||
patient_name VARCHAR(50),
|
||||
discharge_date DATE,
|
||||
primary_diagnosis VARCHAR(200),
|
||||
primary_diagnosis_code VARCHAR(20),
|
||||
secondary_diagnosis_code VARCHAR(20),
|
||||
primary_procedure VARCHAR(200),
|
||||
primary_procedure_code VARCHAR(20),
|
||||
drg_code VARCHAR(20),
|
||||
drg_name VARCHAR(200),
|
||||
drg_weight DECIMAL(8,4),
|
||||
dip_code VARCHAR(20),
|
||||
dip_name VARCHAR(200),
|
||||
grouping_type VARCHAR(10) NOT NULL,
|
||||
total_cost DECIMAL(12,2),
|
||||
insurance_payment DECIMAL(12,2),
|
||||
patient_payment DECIMAL(12,2),
|
||||
los_days INT,
|
||||
grouping_result TEXT,
|
||||
is_valid BOOLEAN DEFAULT TRUE,
|
||||
invalid_reason VARCHAR(200),
|
||||
tenant_id BIGINT DEFAULT 0,
|
||||
is_deleted INT NOT NULL DEFAULT 0,
|
||||
create_time TIMESTAMP DEFAULT CURRENT CURRENT_TIMESTAMP
|
||||
);
|
||||
COMMENT ON TABLE mr_drg_grouping IS 'DRG/DIP分组结果';
|
||||
COMMENT ON COLUMN mr_drg_grouping.grouping_type IS '分组类型(DRG/DIP)';
|
||||
CREATE INDEX idx_drg_encounter ON mr_drg_grouping(encounter_id);
|
||||
CREATE INDEX idx_drg_code ON mr_drg_grouping(drg_code);
|
||||
CREATE INDEX idx_dip_code ON mr_drg_grouping(dip_code);
|
||||
|
||||
-- 病案归档统计表
|
||||
CREATE TABLE IF NOT EXISTS mr_archive_stats (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
stat_date DATE NOT NULL,
|
||||
department_id BIGINT,
|
||||
department_name VARCHAR(100),
|
||||
total_discharged INT DEFAULT 0,
|
||||
archived_count INT DEFAULT 0,
|
||||
archive_rate DECIMAL(5,2),
|
||||
avg_archive_hours DECIMAL(8,2),
|
||||
tenant_id BIGINT DEFAULT 0,
|
||||
create_time TIMESTAMP DEFAULT CURRENT CURRENT_TIMESTAMP
|
||||
);
|
||||
COMMENT ON TABLE mr_archive_stats IS '病案归档统计(每日)';
|
||||
CREATE UNIQUE INDEX idx_mras_date_dept ON mr_archive_stats(stat_date, department_id);
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.healthlink.his.mrhomepage.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.core.common.core.domain.HisBaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* DRG/DIP分组结果
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("mr_drg_grouping")
|
||||
public class MrDrgGrouping extends HisBaseEntity {
|
||||
@TableId(value = "id", type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long encounterId;
|
||||
private Long patientId;
|
||||
private String patientName;
|
||||
private LocalDate dischargeDate;
|
||||
private String primaryDiagnosis;
|
||||
private String primaryDiagnosisCode;
|
||||
private String secondaryDiagnosisCode;
|
||||
private String primaryProcedure;
|
||||
private String primaryProcedureCode;
|
||||
private String drgCode;
|
||||
private String drgName;
|
||||
private BigDecimal drgWeight;
|
||||
private String dipCode;
|
||||
private String dipName;
|
||||
private String groupingType;
|
||||
private BigDecimal totalCost;
|
||||
private BigDecimal insurancePayment;
|
||||
private BigDecimal patientPayment;
|
||||
private Integer losDays;
|
||||
private String groupingResult;
|
||||
private Boolean isValid;
|
||||
private String invalidReason;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.healthlink.his.mrhomepage.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.healthlink.his.mrhomepage.domain.MrDrgGrouping;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface MrDrgGroupingMapper extends BaseMapper<MrDrgGrouping> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.healthlink.his.mrhomepage.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.healthlink.his.mrhomepage.domain.MrDrgGrouping;
|
||||
|
||||
public interface IMrDrgGroupingService extends IService<MrDrgGrouping> {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.healthlink.his.mrhomepage.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.healthlink.his.mrhomepage.domain.MrDrgGrouping;
|
||||
import com.healthlink.his.mrhomepage.mapper.MrDrgGroupingMapper;
|
||||
import com.healthlink.his.mrhomepage.service.IMrDrgGroupingService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class MrDrgGroupingServiceImpl
|
||||
extends ServiceImpl<MrDrgGroupingMapper, MrDrgGrouping>
|
||||
implements IMrDrgGroupingService {
|
||||
}
|
||||
6
healthlink-his-ui/src/views/mrhomepage/drg/api.js
Normal file
6
healthlink-his-ui/src/views/mrhomepage/drg/api.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import request from '@/utils/request'
|
||||
export function getPage(p){return request({url:'/mr-drg/page',method:'get',params:p})}
|
||||
export function groupDrg(d){return request({url:'/mr-drg/group',method:'post',data:d})}
|
||||
export function invalidate(id,reason){return request({url:'/mr-drg/invalidate/'+id,method:'put',params:{reason}})}
|
||||
export function getOverview(p){return request({url:'/mr-drg/stats/overview',method:'get',params:p})}
|
||||
export function getTopDrg(p){return request({url:'/mr-drg/stats/top-drg',method:'get',params:p})}
|
||||
91
healthlink-his-ui/src/views/mrhomepage/drg/index.vue
Normal file
91
healthlink-his-ui/src/views/mrhomepage/drg/index.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<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="loadStats">刷新统计</el-button>
|
||||
</div>
|
||||
<el-row :gutter="12" style="margin-bottom:16px">
|
||||
<el-col :span="4"><el-card shadow="hover"><div style="text-align:center"><div style="font-size:20px;font-weight:bold;color:#409eff">{{ overview.drgTotal||0 }}</div><div style="font-size:12px;color:#999">DRG分组</div></div></el-card></el-col>
|
||||
<el-col :span="4"><el-card shadow="hover"><div style="text-align:center"><div style="font-size:20px;font-weight:bold;color:#67c23a">{{ overview.dipTotal||0 }}</div><div style="font-size:12px;color:#999">DIP分组</div></div></el-card></el-col>
|
||||
<el-col :span="4"><el-card shadow="hover"><div style="text-align:center"><div style="font-size:20px;font-weight:bold;color:#f56c6c">{{ overview.invalidCount||0 }}</div><div style="font-size:12px;color:#999">无效分组</div></div></el-card></el-col>
|
||||
<el-col :span="6"><el-card shadow="hover"><div style="text-align:center"><div style="font-size:20px;font-weight:bold;color:#e6a23c">¥{{ (overview.totalCost||0).toLocaleString() }}</div><div style="font-size:12px;color:#999">总费用</div></div></el-card></el-col>
|
||||
<el-col :span="6"><el-card shadow="hover"><div style="text-align:center"><div style="font-size:20px;font-weight:bold;color:#909399">¥{{ (overview.totalInsurance||0).toLocaleString() }}</div><div style="font-size:12px;color:#999">医保支付</div></div></el-card></el-col>
|
||||
</el-row>
|
||||
<el-tabs v-model="activeTab" type="border-card">
|
||||
<el-tab-pane label="分组列表" name="list">
|
||||
<div style="margin-bottom:12px;display:flex;gap:8px">
|
||||
<el-select v-model="q.groupingType" placeholder="分组类型" clearable style="width:120px">
|
||||
<el-option label="DRG" value="DRG"/><el-option label="DIP" value="DIP"/>
|
||||
</el-select>
|
||||
<el-input v-model="q.patientName" placeholder="患者" clearable style="width:140px"/>
|
||||
<el-button type="primary" @click="loadData">查询</el-button>
|
||||
<el-button type="success" @click="openGroup">新建分组</el-button>
|
||||
</div>
|
||||
<el-table :data="tableData" border stripe>
|
||||
<el-table-column prop="patientName" label="患者" width="100"/>
|
||||
<el-table-column prop="groupingType" label="类型" width="70" align="center">
|
||||
<template #default="{row}"><el-tag :type="row.groupingType==='DRG'?'primary':'success'" size="small">{{ row.groupingType }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="drgCode" label="DRG编码" width="120"/>
|
||||
<el-table-column prop="drgName" label="DRG名称" width="150" show-overflow-tooltip/>
|
||||
<el-table-column prop="drgWeight" label="权重" width="70" align="center"/>
|
||||
<el-table-column prop="primaryDiagnosis" label="主诊断" width="150" show-overflow-tooltip/>
|
||||
<el-table-column prop="totalCost" label="总费用" width="100" align="right">
|
||||
<template #default="{row}">¥{{ row.totalCost }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="losDays" label="住院天数" width="80" align="center"/>
|
||||
<el-table-column prop="isValid" label="有效" width="60" align="center">
|
||||
<template #default="{row}"><el-tag :type="row.isValid?'success':'danger'" size="small">{{ row.isValid?'是':'否' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total,prev,pager,next" @current-change="loadData"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="DRG排名" name="top">
|
||||
<el-table :data="topDrg" border stripe>
|
||||
<el-table-column type="index" label="排名" width="70"/>
|
||||
<el-table-column prop="drgCode" label="DRG编码" width="120"/>
|
||||
<el-table-column prop="count" label="病例数" width="80" align="center"/>
|
||||
<el-table-column prop="totalCost" label="总费用" width="120" align="right">
|
||||
<template #default="{row}">¥{{ row.totalCost }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-dialog v-model="groupDialog" title="新建DRG/DIP分组" width="600px">
|
||||
<el-form :model="groupForm" label-width="100px">
|
||||
<el-form-item label="患者ID"><el-input v-model.number="groupForm.patientId"/></el-form-item>
|
||||
<el-form-item label="就诊ID"><el-input v-model.number="groupForm.encounterId"/></el-form-item>
|
||||
<el-form-item label="患者姓名"><el-input v-model="groupForm.patientName"/></el-form-item>
|
||||
<el-form-item label="出院日期"><el-date-picker v-model="groupForm.dischargeDate" type="date"/></el-form-item>
|
||||
<el-form-item label="主诊断"><el-input v-model="groupForm.primaryDiagnosis"/></el-form-item>
|
||||
<el-form-item label="主诊断编码"><el-input v-model="groupForm.primaryDiagnosisCode"/></el-form-item>
|
||||
<el-form-item label="主手术"><el-input v-model="groupForm.primaryProcedure"/></el-form-item>
|
||||
<el-form-item label="主手术编码"><el-input v-model="groupForm.primaryProcedureCode"/></el-form-item>
|
||||
<el-form-item label="分组类型">
|
||||
<el-radio-group v-model="groupForm.groupingType"><el-radio value="DRG">DRG</el-radio><el-radio value="DIP">DIP</el-radio></el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="总费用"><el-input-number v-model="groupForm.totalCost" :min="0" :precision="2"/></el-form-item>
|
||||
<el-form-item label="住院天数"><el-input-number v-model="groupForm.losDays" :min="1"/></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="groupDialog=false">取消</el-button>
|
||||
<el-button type="primary" @click="doGroup">执行分组</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {ref,onMounted} from 'vue'
|
||||
import {ElMessage} from 'element-plus'
|
||||
import {getPage,groupDrg,getOverview,getTopDrg} from './api'
|
||||
const activeTab=ref('list')
|
||||
const tableData=ref([]);const total=ref(0);const overview=ref({});const topDrg=ref([])
|
||||
const q=ref({pageNo:1,pageSize:20,groupingType:'',patientName:''})
|
||||
const groupDialog=ref(false)
|
||||
const groupForm=ref({patientId:null,encounterId:null,patientName:'',dischargeDate:null,primaryDiagnosis:'',primaryDiagnosisCode:'',primaryProcedure:'',primaryProcedureCode:'',groupingType:'DRG',totalCost:0,losDays:1})
|
||||
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
|
||||
const loadStats=async()=>{const r=await getOverview();overview.value=r.data||{};const t=await getTopDrg();topDrg.value=t.data||[]}
|
||||
const openGroup=()=>{groupDialog.value=true}
|
||||
const doGroup=async()=>{await groupDrg(groupForm.value);ElMessage.success('分组完成');groupDialog.value=false;loadData();loadStats()}
|
||||
onMounted(()=>{loadData();loadStats()})
|
||||
</script>
|
||||
Reference in New Issue
Block a user