feat(rationaldrug): T11.4 肝肾功能自动调量

- 新增 DosageAdjustmentRequestDto 请求DTO
- IRationalDrugAppService 新增 adjustDosageByOrganFunction 方法
- RationalDrugAppServiceImpl 实现肝肾功能评估 + 剂量匹配逻辑
- RationalDrugController 新增 POST /adjust-dosage 端点
- rationaldrug.js 新增前端 API 函数
- 新建 DosageAdjustment.vue 肝肾功能输入 + 调量建议展示
This commit is contained in:
2026-06-18 15:10:26 +08:00
parent abafd4b2a9
commit c004badf30
6 changed files with 575 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ import com.healthlink.his.rationaldrug.domain.DrugInteractionRule;
import com.healthlink.his.rationaldrug.domain.DrugDosageRange;
import com.healthlink.his.rationaldrug.dto.AuditResultDto;
import com.healthlink.his.rationaldrug.dto.AuditStatisticsDto;
import com.healthlink.his.rationaldrug.dto.DosageAdjustmentRequestDto;
import com.healthlink.his.rationaldrug.dto.InteractionCheckResultDto;
import com.healthlink.his.rationaldrug.dto.PrescriptionAuditDto;
@@ -74,4 +75,12 @@ public interface IRationalDrugAppService {
* @return 审核结果
*/
AuditResultDto checkDosage(String drugCode, BigDecimal dosage, String population);
/**
* 根据肝肾功能自动建议调量
*
* @param request 调量请求
* @return 调量建议结果
*/
AuditResultDto adjustDosageByOrganFunction(DosageAdjustmentRequestDto request);
}

View File

@@ -6,6 +6,7 @@ import com.healthlink.his.rationaldrug.domain.DrugInteractionRule;
import com.healthlink.his.rationaldrug.domain.PrescriptionAuditLog;
import com.healthlink.his.rationaldrug.dto.AuditResultDto;
import com.healthlink.his.rationaldrug.dto.AuditStatisticsDto;
import com.healthlink.his.rationaldrug.dto.DosageAdjustmentRequestDto;
import com.healthlink.his.rationaldrug.dto.InteractionCheckResultDto;
import com.healthlink.his.rationaldrug.dto.PrescriptionAuditDto;
import com.healthlink.his.rationaldrug.service.IDrugDosageRangeService;
@@ -257,6 +258,218 @@ public class RationalDrugAppServiceImpl implements IRationalDrugAppService {
return buildResult("PASS", null, 0, "剂量在合理范围内", null);
}
/**
* 根据肝肾功能自动建议调量
* <p>
* 流程:评估肝肾功能损害程度 → 匹配药品剂量规则 → 比较当前剂量 → 生成调量建议
* </p>
*/
@Override
public AuditResultDto adjustDosageByOrganFunction(DosageAdjustmentRequestDto request) {
String drugCode = request.getDrugCode();
String organType = request.getOrganFunctionType();
String population = request.getPopulation();
if (drugCode == null || organType == null) {
return buildResult("MANUAL", "INVALID_INPUT", 1,
"药品编码和肝肾功能类型不能为空",
"请提供完整的药品编码和肝肾功能类型");
}
// 1. 评估器官功能损害程度
String impairmentLevel = assessImpairmentLevel(request);
if ("NORMAL".equals(impairmentLevel)) {
return buildResult("PASS", null, 0,
"肝肾功能正常,当前剂量无需调整",
"无需调量");
}
// 2. 查询该药品的剂量规则
LambdaQueryWrapper<DrugDosageRange> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DrugDosageRange::getDrugCode, drugCode)
.eq(DrugDosageRange::getEnabled, "1")
.eq(DrugDosageRange::getDelFlag, "0");
if (population != null) {
wrapper.eq(DrugDosageRange::getPopulation, population);
}
List<DrugDosageRange> ranges = drugDosageRangeService.list(wrapper);
if (ranges.isEmpty()) {
return buildResult("MANUAL", "DOSAGE_RANGE_NOT_FOUND", 1,
"药品 " + drugCode + " 无剂量范围规则,请先维护剂量规则",
"建议人工确认剂量合理性");
}
// 3. 根据损害程度生成调量建议
BigDecimal currentDose = request.getCurrentDose();
DrugDosageRange matchedRange = ranges.get(0);
StringBuilder detail = new StringBuilder();
detail.append("肝肾功能评估: ").append(formatOrganType(organType));
detail.append("").append(formatImpairmentLevel(impairmentLevel));
String suggestion;
String ruleHit;
if (currentDose == null) {
// 未提供当前剂量,仅给出通用建议
suggestion = "基于" + formatImpairmentLevel(impairmentLevel) + ""
+ "建议参考剂量范围 " + matchedRange.getMinDose() + "-"
+ matchedRange.getMaxDose() + matchedRange.getDoseUnit();
if (matchedRange.getAdjustmentNote() != null) {
suggestion += "" + matchedRange.getAdjustmentNote();
}
ruleHit = "DOSE_NOT_PROVIDED";
detail.append("。未提供当前剂量,给出通用调量建议");
} else {
BigDecimal minDose = matchedRange.getMinDose();
BigDecimal maxDose = matchedRange.getMaxDose();
if (currentDose.compareTo(minDose) >= 0 && currentDose.compareTo(maxDose) <= 0) {
// 当前剂量在正常范围内,但器官功能受损,仍建议减量
if ("SEVERE".equals(impairmentLevel)) {
BigDecimal reducedDose = currentDose.multiply(BigDecimal.valueOf(0.5))
.setScale(1, RoundingMode.HALF_UP);
suggestion = "当前剂量 " + currentDose + matchedRange.getDoseUnit()
+ " 虽在常规范围内,但" + formatImpairmentLevel(impairmentLevel)
+ ",建议减量至 " + reducedDose + matchedRange.getDoseUnit();
if (matchedRange.getAdjustmentNote() != null) {
suggestion += "" + matchedRange.getAdjustmentNote();
}
ruleHit = "SEVERE_IMPAIRMENT_REDUCE";
detail.append("。当前剂量在常规范围内,但重度损害需减量");
} else if ("MODERATE".equals(impairmentLevel)) {
BigDecimal reducedDose = currentDose.multiply(BigDecimal.valueOf(0.75))
.setScale(1, RoundingMode.HALF_UP);
suggestion = "当前剂量 " + currentDose + matchedRange.getDoseUnit()
+ " 虽在常规范围内,但" + formatImpairmentLevel(impairmentLevel)
+ ",建议减量至 " + reducedDose + matchedRange.getDoseUnit();
if (matchedRange.getAdjustmentNote() != null) {
suggestion += "" + matchedRange.getAdjustmentNote();
}
ruleHit = "MODERATE_IMPAIRMENT_REDUCE";
detail.append("。当前剂量在常规范围内,中度损害建议适当减量");
} else {
suggestion = "当前剂量 " + currentDose + matchedRange.getDoseUnit()
+ " 在常规范围内,轻度" + formatOrganType(organType) + "损害,暂无需调整,建议密切监测";
ruleHit = "MILD_IMPAIRMENT_MONITOR";
detail.append("。轻度损害,当前剂量可维持");
}
} else if (currentDose.compareTo(maxDose) > 0) {
// 当前剂量超出范围
suggestion = "当前剂量 " + currentDose + matchedRange.getDoseUnit()
+ " 超过推荐范围 " + minDose + "-" + maxDose + matchedRange.getDoseUnit()
+ ",且存在" + formatImpairmentLevel(impairmentLevel)
+ ",强烈建议减量至 " + minDose + "-" + maxDose + matchedRange.getDoseUnit();
if (matchedRange.getAdjustmentNote() != null) {
suggestion += "" + matchedRange.getAdjustmentNote();
}
ruleHit = "DOSAGE_EXCEEDS_RANGE_WITH_IMPAIRMENT";
detail.append("。当前剂量超出推荐范围,叠加器官损害风险更高");
} else {
// 当前剂量低于范围
suggestion = "当前剂量 " + currentDose + matchedRange.getDoseUnit()
+ " 低于推荐范围 " + minDose + "-" + maxDose + matchedRange.getDoseUnit()
+ ",结合" + formatImpairmentLevel(impairmentLevel)
+ ",建议调整至 " + minDose + "-" + maxDose + matchedRange.getDoseUnit();
ruleHit = "DOSAGE_BELOW_RANGE_WITH_IMPAIRMENT";
detail.append("。当前剂量偏低,结合器官损害情况建议调整");
}
}
return buildResult("MANUAL", ruleHit, 1, detail.toString(), suggestion);
}
/**
* 评估器官功能损害程度
*
* @param request 调量请求
* @return 损害程度: NORMAL / MILD / MODERATE / SEVERE
*/
private String assessImpairmentLevel(DosageAdjustmentRequestDto request) {
String organType = request.getOrganFunctionType();
String level = "NORMAL";
if ("KIDNEY".equals(organType) || "BOTH".equals(organType)) {
String kidneyLevel = assessKidneyFunction(request.getEgfr(), request.getCreatinine());
level = mergeLevel(level, kidneyLevel);
}
if ("LIVER".equals(organType) || "BOTH".equals(organType)) {
String liverLevel = assessLiverFunction(request.getAlt(), request.getAst());
level = mergeLevel(level, liverLevel);
}
return level;
}
private String assessKidneyFunction(java.math.BigDecimal egfr, java.math.BigDecimal creatinine) {
if (egfr != null) {
if (egfr.compareTo(java.math.BigDecimal.valueOf(90)) >= 0) return "NORMAL";
if (egfr.compareTo(java.math.BigDecimal.valueOf(60)) >= 0) return "MILD";
if (egfr.compareTo(java.math.BigDecimal.valueOf(30)) >= 0) return "MODERATE";
return "SEVERE";
}
if (creatinine != null) {
if (creatinine.compareTo(java.math.BigDecimal.valueOf(133)) < 0) return "NORMAL";
if (creatinine.compareTo(java.math.BigDecimal.valueOf(177)) < 0) return "MILD";
if (creatinine.compareTo(java.math.BigDecimal.valueOf(442)) < 0) return "MODERATE";
return "SEVERE";
}
return "NORMAL";
}
private String assessLiverFunction(java.math.BigDecimal alt, java.math.BigDecimal ast) {
int level = 0;
if (alt != null) {
if (alt.compareTo(java.math.BigDecimal.valueOf(40)) >= 0) level = Math.max(level, 1);
if (alt.compareTo(java.math.BigDecimal.valueOf(120)) >= 0) level = Math.max(level, 2);
if (alt.compareTo(java.math.BigDecimal.valueOf(400)) >= 0) level = Math.max(level, 3);
}
if (ast != null) {
if (ast.compareTo(java.math.BigDecimal.valueOf(40)) >= 0) level = Math.max(level, 1);
if (ast.compareTo(java.math.BigDecimal.valueOf(120)) >= 0) level = Math.max(level, 2);
if (ast.compareTo(java.math.BigDecimal.valueOf(400)) >= 0) level = Math.max(level, 3);
}
return switch (level) {
case 1 -> "MILD";
case 2 -> "MODERATE";
case 3 -> "SEVERE";
default -> "NORMAL";
};
}
private String mergeLevel(String current, String candidate) {
int currentLevel = impairmentLevelValue(current);
int candidateLevel = impairmentLevelValue(candidate);
return candidateLevel > currentLevel ? candidate : current;
}
private int impairmentLevelValue(String level) {
return switch (level) {
case "SEVERE" -> 3;
case "MODERATE" -> 2;
case "MILD" -> 1;
default -> 0;
};
}
private String formatOrganType(String organType) {
return switch (organType) {
case "LIVER" -> "肝功能";
case "KIDNEY" -> "肾功能";
case "BOTH" -> "肝肾功能";
default -> organType;
};
}
private String formatImpairmentLevel(String level) {
return switch (level) {
case "MILD" -> "轻度损害";
case "MODERATE" -> "中度损害";
case "SEVERE" -> "重度损害";
default -> "正常";
};
}
/**
* 保存审核日志
*/

View File

@@ -5,6 +5,7 @@ import com.healthlink.his.rationaldrug.domain.DrugDosageRange;
import com.healthlink.his.rationaldrug.domain.DrugInteractionRule;
import com.healthlink.his.rationaldrug.dto.AuditResultDto;
import com.healthlink.his.rationaldrug.dto.AuditStatisticsDto;
import com.healthlink.his.rationaldrug.dto.DosageAdjustmentRequestDto;
import com.healthlink.his.rationaldrug.dto.InteractionCheckResultDto;
import com.healthlink.his.rationaldrug.dto.PrescriptionAuditDto;
import com.healthlink.his.rationaldrug.service.IDrugDosageRangeService;
@@ -14,6 +15,7 @@ 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.math.BigDecimal;
@@ -127,4 +129,12 @@ public class RationalDrugController {
AuditResultDto result = rationalDrugAppService.checkDosage(drugCode, dosage, population);
return AjaxResult.success(result);
}
@PostMapping("/adjust-dosage")
@Operation(summary = "肝肾功能自动调量")
@PreAuthorize("hasAuthority('infection:rationaldrug:edit')")
public AjaxResult adjustDosageByOrganFunction(@RequestBody DosageAdjustmentRequestDto request) {
AuditResultDto result = rationalDrugAppService.adjustDosageByOrganFunction(request);
return AjaxResult.success(result);
}
}

View File

@@ -0,0 +1,43 @@
package com.healthlink.his.rationaldrug.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
/**
* 肝肾功能调量请求DTO
*
* @author system
*/
@Data
@Accessors(chain = true)
public class DosageAdjustmentRequestDto {
/** 药品编码 */
private String drugCode;
/** 当前剂量 */
private BigDecimal currentDose;
/** 剂量单位 */
private String doseUnit;
/** 肝肾功能类型: LIVER / KIDNEY / BOTH */
private String organFunctionType;
/** 肌酐 (μmol/L) — 肾功能 */
private BigDecimal creatinine;
/** 肾小球滤过率 eGFR (mL/min) — 肾功能 */
private BigDecimal egfr;
/** 谷丙转氨酶 ALT (U/L) — 肝功能 */
private BigDecimal alt;
/** 谷草转氨酶 AST (U/L) — 肝功能 */
private BigDecimal ast;
/** 人群类型: ADULT/CHILD/ELDERLY/PREGNANT */
private String population;
}

View File

@@ -50,3 +50,8 @@ export function listDosageRules(params) {
export function checkDosage(drugCode, dosage, population) {
return request({ url: '/api/v1/rational-drug/check-dosage', method: 'get', params: { drugCode, dosage, population } })
}
// ==================== 肝肾功能调量 ====================
export function adjustDosageByOrganFunction(data) {
return request({ url: '/api/v1/rational-drug/adjust-dosage', method: 'post', data })
}

View File

@@ -0,0 +1,295 @@
<template>
<div class="app-container">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>肝肾功能自动调量</span>
</div>
</template>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="120px"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item
label="药品编码"
prop="drugCode"
>
<el-input
v-model="form.drugCode"
placeholder="请输入药品编码"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="人群类型"
prop="population"
>
<el-select
v-model="form.population"
placeholder="请选择人群类型"
style="width: 100%"
>
<el-option
v-for="item in populationOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item
label="当前剂量"
prop="currentDose"
>
<el-input-number
v-model="form.currentDose"
:min="0"
:precision="1"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item
label="剂量单位"
prop="doseUnit"
>
<el-input
v-model="form.doseUnit"
placeholder="如 mg、ml"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item
label="肝肾功能类型"
prop="organFunctionType"
>
<el-select
v-model="form.organFunctionType"
placeholder="请选择"
style="width: 100%"
>
<el-option
v-for="item in organTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">
肾功能指标
</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="肌酐 (μmol/L)">
<el-input-number
v-model="form.creatinine"
:min="0"
:precision="1"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="eGFR (mL/min)">
<el-input-number
v-model="form.egfr"
:min="0"
:precision="1"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">
肝功能指标
</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="ALT (U/L)">
<el-input-number
v-model="form.alt"
:min="0"
:precision="1"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="AST (U/L)">
<el-input-number
v-model="form.ast"
:min="0"
:precision="1"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button
type="primary"
:loading="loading"
@click="handleSubmit"
>
生成调量建议
</el-button>
<el-button @click="handleReset">
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card
v-if="result"
shadow="never"
class="mt20"
>
<template #header>
<div class="card-header">
<span>调量建议结果</span>
<el-tag :type="resultTagType(result.auditResult)">
{{ resultText(result.auditResult) }}
</el-tag>
</div>
</template>
<el-descriptions
:column="1"
border
>
<el-descriptions-item label="审核结果">
<el-tag :type="resultTagType(result.auditResult)">
{{ resultText(result.auditResult) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="规则命中">
{{ result.ruleHit || '无' }}
</el-descriptions-item>
<el-descriptions-item label="详细评估">
{{ result.detail || '无' }}
</el-descriptions-item>
<el-descriptions-item label="调量建议">
<el-alert
:title="result.suggestion || '无需调整'"
:type="result.auditResult === 'PASS' ? 'success' : 'warning'"
show-icon
:closable="false"
/>
</el-descriptions-item>
</el-descriptions>
</el-card>
</div>
</template>
<script setup name="DosageAdjustment" lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { adjustDosageByOrganFunction } from '@/api/rationaldrug'
const formRef = ref(null)
const loading = ref(false)
const result = ref(null)
const form = reactive({
drugCode: '',
population: 'ADULT',
currentDose: null,
doseUnit: 'mg',
organFunctionType: 'KIDNEY',
creatinine: null,
egfr: null,
alt: null,
ast: null
})
const rules = reactive({
drugCode: [{ required: true, message: '药品编码不能为空', trigger: 'blur' }],
organFunctionType: [{ required: true, message: '请选择肝肾功能类型', trigger: 'change' }]
})
const populationOptions = [
{ label: '成人', value: 'ADULT' },
{ label: '儿童', value: 'CHILD' },
{ label: '老年', value: 'ELDERLY' },
{ label: '孕妇', value: 'PREGNANT' }
]
const organTypeOptions = [
{ label: '肾功能', value: 'KIDNEY' },
{ label: '肝功能', value: 'LIVER' },
{ label: '肝肾功能', value: 'BOTH' }
]
function resultTagType(auditResult) {
const map = { PASS: 'success', REJECT: 'danger', MANUAL: 'warning' }
return map[auditResult] || 'info'
}
function resultText(auditResult) {
const map = { PASS: '通过', REJECT: '拒绝', MANUAL: '需人工复核' }
return map[auditResult] || '未知'
}
function handleSubmit() {
formRef.value?.validate(valid => {
if (!valid) return
loading.value = true
adjustDosageByOrganFunction(form).then(res => {
result.value = res.data
ElMessage.success('调量评估完成')
}).finally(() => {
loading.value = false
})
})
}
function handleReset() {
formRef.value?.resetFields()
Object.assign(form, {
drugCode: '',
population: 'ADULT',
currentDose: null,
doseUnit: 'mg',
organFunctionType: 'KIDNEY',
creatinine: null,
egfr: null,
alt: null,
ast: null
})
result.value = null
}
</script>
<style lang="scss" scoped>
.app-container {
padding: 20px;
}
.mt20 {
margin-top: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>