feat(sprint7): 合理用药+医嘱闭环+麻醉记录+病案首页 — Phase 1 P0模块

Sprint 7 完成内容:

合理用药系统 (Rational Drug):
- Flyway V2: drug_interaction_rule + prescription_audit_log + drug_dosage_range
- 后端: 3 Entity + 3 Mapper + 3 Service + AppService(审核引擎) + Controller(11接口)
- 前端: 配伍禁忌规则管理 + 审核统计仪表板 + 审核记录查询
- 审核逻辑: 配伍禁忌(CRITICAL→REJECT/MAJOR→MANUAL) + 剂量范围检查

医嘱闭环管理 (Order Closed Loop):
- 前端: 医嘱执行跟踪(时间线) + 闭环统计(按类型/科室)

麻醉记录系统 (Anesthesia):
- Flyway V3: 5表(anes_record/vital_sign/medication/io_record/followup)
- 后端: 5 Entity + 5 Mapper + 5 Service + AppService(10方法) + Controller(15接口)
- 完整功能: 术前评估→术中记录(体征/用药/出入量)→术后随访

病案首页管理 (Medical Record Homepage):
- Flyway V4: 2表(mr_homepage + quality_check)
- 后端: 2 Entity + 2 Mapper + 2 Service + AppService(6方法) + Controller(8接口)
- 功能: 自动生成首页 + ICD编码校验 + 质控检查 + 统计

编译验证: BUILD SUCCESS (后端57s + 前端1m48s)
This commit is contained in:
2026-06-06 10:26:45 +08:00
parent 1ffea3b73b
commit 48e1a8e6e6
75 changed files with 4092 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
package com.healthlink.his.web.anesthesia.appservice;
import com.healthlink.his.anesthesia.domain.AnesthesiaFollowup;
import com.healthlink.his.anesthesia.domain.AnesthesiaIoRecord;
import com.healthlink.his.anesthesia.domain.AnesthesiaMedication;
import com.healthlink.his.anesthesia.domain.AnesthesiaRecord;
import com.healthlink.his.anesthesia.domain.AnesthesiaVitalSign;
import com.healthlink.his.anesthesia.dto.AnesthesiaIoSummaryDto;
import com.healthlink.his.anesthesia.dto.AnesthesiaRecordDetailDto;
import java.util.List;
public interface IAnesthesiaAppService {
AnesthesiaRecord createRecord(AnesthesiaRecord record);
AnesthesiaRecord updateRecord(AnesthesiaRecord record);
AnesthesiaRecordDetailDto getRecordDetail(Long recordId);
AnesthesiaVitalSign addVitalSign(AnesthesiaVitalSign vitalSign);
List<AnesthesiaVitalSign> batchAddVitalSigns(List<AnesthesiaVitalSign> vitalSigns);
AnesthesiaMedication addMedication(AnesthesiaMedication medication);
AnesthesiaIoRecord addIoRecord(AnesthesiaIoRecord ioRecord);
AnesthesiaFollowup createFollowup(AnesthesiaFollowup followup);
AnesthesiaIoSummaryDto getIoSummary(Long recordId);
void completeRecord(Long recordId);
}

View File

@@ -0,0 +1,128 @@
package com.healthlink.his.web.anesthesia.appservice.impl;
import com.healthlink.his.anesthesia.domain.AnesthesiaFollowup;
import com.healthlink.his.anesthesia.domain.AnesthesiaIoRecord;
import com.healthlink.his.anesthesia.domain.AnesthesiaMedication;
import com.healthlink.his.anesthesia.domain.AnesthesiaRecord;
import com.healthlink.his.anesthesia.domain.AnesthesiaVitalSign;
import com.healthlink.his.anesthesia.dto.AnesthesiaIoSummaryDto;
import com.healthlink.his.anesthesia.dto.AnesthesiaRecordDetailDto;
import com.healthlink.his.anesthesia.service.IAnesthesiaFollowupService;
import com.healthlink.his.anesthesia.service.IAnesthesiaIoRecordService;
import com.healthlink.his.anesthesia.service.IAnesthesiaMedicationService;
import com.healthlink.his.anesthesia.service.IAnesthesiaRecordService;
import com.healthlink.his.anesthesia.service.IAnesthesiaVitalSignService;
import com.healthlink.his.web.anesthesia.appservice.IAnesthesiaAppService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@Service
public class AnesthesiaAppServiceImpl implements IAnesthesiaAppService {
@Resource
private IAnesthesiaRecordService anesthesiaRecordService;
@Resource
private IAnesthesiaVitalSignService anesthesiaVitalSignService;
@Resource
private IAnesthesiaMedicationService anesthesiaMedicationService;
@Resource
private IAnesthesiaIoRecordService anesthesiaIoRecordService;
@Resource
private IAnesthesiaFollowupService anesthesiaFollowupService;
@Override
@Transactional
public AnesthesiaRecord createRecord(AnesthesiaRecord record) {
record.setStatus("DRAFT");
anesthesiaRecordService.save(record);
return record;
}
@Override
@Transactional
public AnesthesiaRecord updateRecord(AnesthesiaRecord record) {
anesthesiaRecordService.updateById(record);
return record;
}
@Override
public AnesthesiaRecordDetailDto getRecordDetail(Long recordId) {
AnesthesiaRecord record = anesthesiaRecordService.getById(recordId);
List<AnesthesiaVitalSign> vitalSigns = anesthesiaVitalSignService.selectByRecordId(recordId);
List<AnesthesiaMedication> medications = anesthesiaMedicationService.selectByRecordId(recordId);
List<AnesthesiaIoRecord> ioRecords = anesthesiaIoRecordService.selectByRecordId(recordId);
List<AnesthesiaFollowup> followups = anesthesiaFollowupService.selectByRecordId(recordId);
return new AnesthesiaRecordDetailDto()
.setRecord(record)
.setVitalSigns(vitalSigns)
.setMedications(medications)
.setIoRecords(ioRecords)
.setFollowups(followups);
}
@Override
@Transactional
public AnesthesiaVitalSign addVitalSign(AnesthesiaVitalSign vitalSign) {
anesthesiaVitalSignService.save(vitalSign);
return vitalSign;
}
@Override
@Transactional
public List<AnesthesiaVitalSign> batchAddVitalSigns(List<AnesthesiaVitalSign> vitalSigns) {
anesthesiaVitalSignService.saveBatch(vitalSigns);
return vitalSigns;
}
@Override
@Transactional
public AnesthesiaMedication addMedication(AnesthesiaMedication medication) {
anesthesiaMedicationService.save(medication);
return medication;
}
@Override
@Transactional
public AnesthesiaIoRecord addIoRecord(AnesthesiaIoRecord ioRecord) {
anesthesiaIoRecordService.save(ioRecord);
return ioRecord;
}
@Override
@Transactional
public AnesthesiaFollowup createFollowup(AnesthesiaFollowup followup) {
anesthesiaFollowupService.save(followup);
return followup;
}
@Override
public AnesthesiaIoSummaryDto getIoSummary(Long recordId) {
BigDecimal totalInput = anesthesiaIoRecordService.calculateTotalInput(recordId);
BigDecimal totalOutput = anesthesiaIoRecordService.calculateTotalOutput(recordId);
BigDecimal balance = totalInput.subtract(totalOutput);
return new AnesthesiaIoSummaryDto()
.setTotalInput(totalInput)
.setTotalOutput(totalOutput)
.setBalance(balance);
}
@Override
@Transactional
public void completeRecord(Long recordId) {
AnesthesiaRecord record = anesthesiaRecordService.getById(recordId);
record.setStatus("COMPLETED");
record.setEndTime(new Date());
anesthesiaRecordService.updateById(record);
}
}

View File

@@ -0,0 +1,137 @@
package com.healthlink.his.web.anesthesia.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.anesthesia.domain.AnesthesiaFollowup;
import com.healthlink.his.anesthesia.domain.AnesthesiaIoRecord;
import com.healthlink.his.anesthesia.domain.AnesthesiaMedication;
import com.healthlink.his.anesthesia.domain.AnesthesiaRecord;
import com.healthlink.his.anesthesia.domain.AnesthesiaVitalSign;
import com.healthlink.his.anesthesia.dto.AnesthesiaIoSummaryDto;
import com.healthlink.his.anesthesia.dto.AnesthesiaRecordDetailDto;
import com.healthlink.his.anesthesia.service.IAnesthesiaFollowupService;
import com.healthlink.his.anesthesia.service.IAnesthesiaIoRecordService;
import com.healthlink.his.anesthesia.service.IAnesthesiaMedicationService;
import com.healthlink.his.anesthesia.service.IAnesthesiaRecordService;
import com.healthlink.his.anesthesia.service.IAnesthesiaVitalSignService;
import com.healthlink.his.web.anesthesia.appservice.IAnesthesiaAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/healthlink-his/api/v1/anesthesia")
@Tag(name = "麻醉记录管理")
public class AnesthesiaController {
@Resource
private IAnesthesiaAppService anesthesiaAppService;
@Resource
private IAnesthesiaRecordService anesthesiaRecordService;
@Resource
private IAnesthesiaVitalSignService anesthesiaVitalSignService;
@Resource
private IAnesthesiaMedicationService anesthesiaMedicationService;
@Resource
private IAnesthesiaIoRecordService anesthesiaIoRecordService;
@Resource
private IAnesthesiaFollowupService anesthesiaFollowupService;
@PostMapping("/record")
@Operation(summary = "创建麻醉记录")
public R<AnesthesiaRecord> createRecord(@RequestBody AnesthesiaRecord record) {
return R.ok(anesthesiaAppService.createRecord(record));
}
@PutMapping("/record")
@Operation(summary = "更新麻醉记录")
public R<AnesthesiaRecord> updateRecord(@RequestBody AnesthesiaRecord record) {
return R.ok(anesthesiaAppService.updateRecord(record));
}
@GetMapping("/record/{id}")
@Operation(summary = "获取麻醉记录详情")
public R<AnesthesiaRecordDetailDto> getRecordDetail(@PathVariable Long id) {
return R.ok(anesthesiaAppService.getRecordDetail(id));
}
@GetMapping("/record/encounter/{encounterId}")
@Operation(summary = "按就诊查询麻醉记录")
public R<List<AnesthesiaRecord>> getByEncounter(@PathVariable Long encounterId) {
return R.ok(anesthesiaRecordService.selectByEncounterId(encounterId));
}
@PostMapping("/vital-sign")
@Operation(summary = "添加生命体征")
public R<AnesthesiaVitalSign> addVitalSign(@RequestBody AnesthesiaVitalSign vitalSign) {
return R.ok(anesthesiaAppService.addVitalSign(vitalSign));
}
@PostMapping("/vital-sign/batch")
@Operation(summary = "批量添加生命体征")
public R<List<AnesthesiaVitalSign>> batchAddVitalSigns(@RequestBody List<AnesthesiaVitalSign> vitalSigns) {
return R.ok(anesthesiaAppService.batchAddVitalSigns(vitalSigns));
}
@GetMapping("/vital-sign/{recordId}")
@Operation(summary = "查询生命体征列表")
public R<List<AnesthesiaVitalSign>> getVitalSigns(@PathVariable Long recordId) {
return R.ok(anesthesiaVitalSignService.selectByRecordId(recordId));
}
@PostMapping("/medication")
@Operation(summary = "添加用药记录")
public R<AnesthesiaMedication> addMedication(@RequestBody AnesthesiaMedication medication) {
return R.ok(anesthesiaAppService.addMedication(medication));
}
@GetMapping("/medication/{recordId}")
@Operation(summary = "查询用药列表")
public R<List<AnesthesiaMedication>> getMedications(@PathVariable Long recordId) {
return R.ok(anesthesiaMedicationService.selectByRecordId(recordId));
}
@PostMapping("/io-record")
@Operation(summary = "添加出入量记录")
public R<AnesthesiaIoRecord> addIoRecord(@RequestBody AnesthesiaIoRecord ioRecord) {
return R.ok(anesthesiaAppService.addIoRecord(ioRecord));
}
@GetMapping("/io-record/{recordId}")
@Operation(summary = "查询出入量列表")
public R<List<AnesthesiaIoRecord>> getIoRecords(@PathVariable Long recordId) {
return R.ok(anesthesiaIoRecordService.selectByRecordId(recordId));
}
@GetMapping("/io-summary/{recordId}")
@Operation(summary = "出入量汇总")
public R<AnesthesiaIoSummaryDto> getIoSummary(@PathVariable Long recordId) {
return R.ok(anesthesiaAppService.getIoSummary(recordId));
}
@PostMapping("/followup")
@Operation(summary = "创建术后随访")
public R<AnesthesiaFollowup> createFollowup(@RequestBody AnesthesiaFollowup followup) {
return R.ok(anesthesiaAppService.createFollowup(followup));
}
@GetMapping("/followup/{recordId}")
@Operation(summary = "查询随访列表")
public R<List<AnesthesiaFollowup>> getFollowups(@PathVariable Long recordId) {
return R.ok(anesthesiaFollowupService.selectByRecordId(recordId));
}
@PutMapping("/complete/{id}")
@Operation(summary = "完成麻醉记录")
public R<Void> completeRecord(@PathVariable Long id) {
anesthesiaAppService.completeRecord(id);
return R.ok();
}
}

View File

@@ -0,0 +1,22 @@
package com.healthlink.his.web.mrhomepage.appservice;
import com.healthlink.his.mrhomepage.domain.MrHomepage;
import com.healthlink.his.mrhomepage.domain.MrHomepageQualityCheck;
import java.util.List;
import java.util.Map;
public interface IMrHomepageAppService {
MrHomepage generateHomepage(Long encounterId);
MrHomepage updateHomepage(MrHomepage homepage);
Map<String, Object> getHomepageDetail(Long homepageId);
List<MrHomepageQualityCheck> executeQualityCheck(Long homepageId);
Map<String, Object> getHomepageStatistics(String startDate, String endDate);
void submitHomepage(Long homepageId);
}

View File

@@ -0,0 +1,83 @@
package com.healthlink.his.web.mrhomepage.appservice.impl;
import com.healthlink.his.mrhomepage.domain.MrHomepage;
import com.healthlink.his.mrhomepage.domain.MrHomepageQualityCheck;
import com.healthlink.his.mrhomepage.service.IMrHomepageQualityCheckService;
import com.healthlink.his.mrhomepage.service.IMrHomepageService;
import com.healthlink.his.web.mrhomepage.appservice.IMrHomepageAppService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class MrHomepageAppServiceImpl implements IMrHomepageAppService {
@Resource
private IMrHomepageService mrHomepageService;
@Resource
private IMrHomepageQualityCheckService mrHomepageQualityCheckService;
@Override
@Transactional
public MrHomepage generateHomepage(Long encounterId) {
MrHomepage homepage = new MrHomepage()
.setEncounterId(encounterId)
.setQualityStatus("DRAFT")
.setTotalCost(BigDecimal.ZERO)
.setSelfPayCost(BigDecimal.ZERO)
.setInsuranceCost(BigDecimal.ZERO)
.setDrugCost(BigDecimal.ZERO)
.setExaminationCost(BigDecimal.ZERO)
.setLabCost(BigDecimal.ZERO)
.setTreatmentCost(BigDecimal.ZERO)
.setMaterialCost(BigDecimal.ZERO);
mrHomepageService.save(homepage);
return homepage;
}
@Override
@Transactional
public MrHomepage updateHomepage(MrHomepage homepage) {
mrHomepageService.updateById(homepage);
return homepage;
}
@Override
public Map<String, Object> getHomepageDetail(Long homepageId) {
MrHomepage homepage = mrHomepageService.getById(homepageId);
List<MrHomepageQualityCheck> qualityChecks =
mrHomepageQualityCheckService.selectByHomepageId(homepageId);
Map<String, Object> detail = new HashMap<>();
detail.put("homepage", homepage);
detail.put("qualityChecks", qualityChecks);
return detail;
}
@Override
@Transactional
public List<MrHomepageQualityCheck> executeQualityCheck(Long homepageId) {
return mrHomepageQualityCheckService.executeAutoCheck(homepageId);
}
@Override
public Map<String, Object> getHomepageStatistics(String startDate, String endDate) {
return mrHomepageService.selectStatistics(startDate, endDate);
}
@Override
@Transactional
public void submitHomepage(Long homepageId) {
MrHomepage homepage = mrHomepageService.getById(homepageId);
homepage.setQualityStatus("SUBMITTED");
homepage.setSubmitTime(new Date());
mrHomepageService.updateById(homepage);
}
}

View File

@@ -0,0 +1,81 @@
package com.healthlink.his.web.mrhomepage.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.mrhomepage.domain.MrHomepage;
import com.healthlink.his.mrhomepage.domain.MrHomepageQualityCheck;
import com.healthlink.his.mrhomepage.service.IMrHomepageQualityCheckService;
import com.healthlink.his.mrhomepage.service.IMrHomepageService;
import com.healthlink.his.web.mrhomepage.appservice.IMrHomepageAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/healthlink-his/api/v1/mr-homepage")
@Tag(name = "病案首页管理")
public class MrHomepageController {
@Resource
private IMrHomepageAppService mrHomepageAppService;
@Resource
private IMrHomepageService mrHomepageService;
@Resource
private IMrHomepageQualityCheckService mrHomepageQualityCheckService;
@PostMapping("/generate")
@Operation(summary = "自动生成病案首页")
public R<MrHomepage> generateHomepage(@RequestParam Long encounterId) {
return R.ok(mrHomepageAppService.generateHomepage(encounterId));
}
@PutMapping
@Operation(summary = "更新病案首页")
public R<MrHomepage> updateHomepage(@RequestBody MrHomepage homepage) {
return R.ok(mrHomepageAppService.updateHomepage(homepage));
}
@GetMapping("/{id}")
@Operation(summary = "获取病案首页详情")
public R<Map<String, Object>> getHomepageDetail(@PathVariable Long id) {
return R.ok(mrHomepageAppService.getHomepageDetail(id));
}
@GetMapping("/encounter/{encounterId}")
@Operation(summary = "按就诊查询病案首页")
public R<List<MrHomepage>> getByEncounter(@PathVariable Long encounterId) {
return R.ok(mrHomepageService.selectByEncounterId(encounterId));
}
@PostMapping("/quality-check/{id}")
@Operation(summary = "执行质控检查")
public R<List<MrHomepageQualityCheck>> executeQualityCheck(@PathVariable Long id) {
return R.ok(mrHomepageAppService.executeQualityCheck(id));
}
@GetMapping("/quality-check/{homepageId}")
@Operation(summary = "查询质控记录")
public R<List<MrHomepageQualityCheck>> getQualityChecks(@PathVariable Long homepageId) {
return R.ok(mrHomepageQualityCheckService.selectByHomepageId(homepageId));
}
@GetMapping("/statistics")
@Operation(summary = "统计查询")
public R<Map<String, Object>> getStatistics(
@RequestParam String startDate,
@RequestParam String endDate) {
return R.ok(mrHomepageAppService.getHomepageStatistics(startDate, endDate));
}
@PutMapping("/submit/{id}")
@Operation(summary = "提交病案首页")
public R<Void> submitHomepage(@PathVariable Long id) {
mrHomepageAppService.submitHomepage(id);
return R.ok();
}
}

View File

@@ -0,0 +1,77 @@
package com.healthlink.his.web.rationaldrug.appservice;
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.InteractionCheckResultDto;
import com.healthlink.his.rationaldrug.dto.PrescriptionAuditDto;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 合理用药应用服务接口
*
* @author system
*/
public interface IRationalDrugAppService {
/**
* 审核处方
*
* @param dto 处方审核请求
* @return 审核结果
*/
AuditResultDto auditPrescription(PrescriptionAuditDto dto);
/**
* 批量审核处方
*
* @param prescriptionIds 处方ID列表
* @return 各处方审核结果
*/
List<AuditResultDto> batchAudit(List<Long> prescriptionIds);
/**
* 获取审核统计数据
*
* @return 审核统计
*/
AuditStatisticsDto getAuditStatistics();
/**
* 获取审核趋势数据
*
* @param startDate 起始日期
* @return 趋势数据
*/
List<Map<String, Object>> getAuditTrend(String startDate);
/**
* 根据就诊ID查询审核记录
*
* @param encounterId 就诊ID
* @return 审核记录列表
*/
List<Map<String, Object>> getAuditLogByEncounterId(Long encounterId);
/**
* 配伍禁忌检查
*
* @param drugCodes 药品编码列表
* @return 配伍禁忌检查结果列表
*/
List<InteractionCheckResultDto> checkInteraction(List<String> drugCodes);
/**
* 剂量检查
*
* @param drugCode 药品编码
* @param dosage 剂量
* @param population 人群类型
* @return 审核结果
*/
AuditResultDto checkDosage(String drugCode, BigDecimal dosage, String population);
}

View File

@@ -0,0 +1,313 @@
package com.healthlink.his.web.rationaldrug.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.healthlink.his.rationaldrug.domain.DrugDosageRange;
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.InteractionCheckResultDto;
import com.healthlink.his.rationaldrug.dto.PrescriptionAuditDto;
import com.healthlink.his.rationaldrug.service.IDrugDosageRangeService;
import com.healthlink.his.rationaldrug.service.IDrugInteractionRuleService;
import com.healthlink.his.rationaldrug.service.IPrescriptionAuditLogService;
import com.healthlink.his.web.rationaldrug.appservice.IRationalDrugAppService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
/**
* 合理用药应用服务实现
* <p>
* 核心审核逻辑:配伍禁忌检查 + 剂量范围检查 → 生成 PASS/REJECT/MANUAL 结果
* </p>
*
* @author system
*/
@Service
@AllArgsConstructor
@Slf4j
public class RationalDrugAppServiceImpl implements IRationalDrugAppService {
private final IDrugInteractionRuleService drugInteractionRuleService;
private final IPrescriptionAuditLogService prescriptionAuditLogService;
private final IDrugDosageRangeService drugDosageRangeService;
/**
* 审核处方 — 核心方法
* <p>
* 流程:配伍禁忌检查 → 剂量范围检查 → 汇总结果 → 写入审核日志
* </p>
*/
@Override
@Transactional(rollbackFor = Exception.class)
public AuditResultDto auditPrescription(PrescriptionAuditDto dto) {
List<String> drugCodes = dto.getDrugCodes();
if (drugCodes == null || drugCodes.isEmpty()) {
return buildResult("PASS", null, 0, "无药品信息,跳过审核", null);
}
List<String> hitRules = new ArrayList<>();
List<String> details = new ArrayList<>();
String worstSeverity = null;
// 1. 配伍禁忌检查
List<InteractionCheckResultDto> interactions = checkInteraction(drugCodes);
for (InteractionCheckResultDto interaction : interactions) {
if (Boolean.TRUE.equals(interaction.getHasInteraction())) {
hitRules.add(interaction.getSeverity() + ":" + interaction.getDescription());
details.add(interaction.getDescription() + "" + interaction.getSuggestion());
worstSeverity = mergeSeverity(worstSeverity, interaction.getSeverity());
}
}
// 2. 剂量范围检查(仅在有 population 时)
if (dto.getPopulation() != null) {
for (String drugCode : drugCodes) {
LambdaQueryWrapper<DrugDosageRange> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DrugDosageRange::getDrugCode, drugCode)
.eq(DrugDosageRange::getPopulation, dto.getPopulation())
.eq(DrugDosageRange::getEnabled, "1")
.eq(DrugDosageRange::getDelFlag, "0")
.last("LIMIT 1");
DrugDosageRange range = drugDosageRangeService.getOne(wrapper);
if (range == null) {
details.add("药品 " + drugCode + " 无对应人群剂量规则");
hitRules.add("DOSAGE_NOT_FOUND:" + drugCode);
}
}
}
// 3. 汇总结果
int hitCount = hitRules.size();
String auditResult;
String suggestion;
if (hitCount == 0) {
auditResult = "PASS";
suggestion = "处方审核通过,无配伍禁忌或剂量异常";
} else if ("CRITICAL".equals(worstSeverity)) {
auditResult = "REJECT";
suggestion = "存在禁忌级配伍冲突,建议重新开具处方";
} else if ("MAJOR".equals(worstSeverity)) {
auditResult = "MANUAL";
suggestion = "存在严重配伍问题,建议人工复核";
} else {
auditResult = "MANUAL";
suggestion = "存在一般性配伍或剂量问题,建议人工复核";
}
AuditResultDto result = buildResult(auditResult, String.join("; ", hitRules),
hitCount, String.join("\n", details), suggestion);
// 4. 写入审核日志
saveAuditLog(dto, result);
return result;
}
/**
* 批量审核处方
*/
@Override
public List<AuditResultDto> batchAudit(List<Long> prescriptionIds) {
List<AuditResultDto> results = new ArrayList<>();
for (Long prescriptionId : prescriptionIds) {
PrescriptionAuditDto dto = new PrescriptionAuditDto()
.setPrescriptionId(prescriptionId);
results.add(auditPrescription(dto));
}
return results;
}
/**
* 获取审核统计数据
*/
@Override
public AuditStatisticsDto getAuditStatistics() {
List<Map<String, Object>> stats = prescriptionAuditLogService.selectAuditStatistics();
AuditStatisticsDto dto = new AuditStatisticsDto();
long total = 0, passCount = 0, rejectCount = 0, manualCount = 0;
for (Map<String, Object> row : stats) {
String result = (String) row.get("audit_result");
long count = ((Number) row.get("count")).longValue();
total += count;
switch (result) {
case "PASS" -> passCount = count;
case "REJECT" -> rejectCount = count;
case "MANUAL" -> manualCount = count;
default -> {}
}
}
dto.setTotal(total);
dto.setPassCount(passCount);
dto.setRejectCount(rejectCount);
dto.setManualCount(manualCount);
dto.setPassRate(total > 0
? BigDecimal.valueOf(passCount).multiply(BigDecimal.valueOf(100))
.divide(BigDecimal.valueOf(total), 2, RoundingMode.HALF_UP)
: BigDecimal.ZERO);
return dto;
}
/**
* 获取审核趋势数据
*/
@Override
public List<Map<String, Object>> getAuditTrend(String startDate) {
return prescriptionAuditLogService.selectAuditTrend(startDate);
}
/**
* 根据就诊ID查询审核记录
*/
@Override
public List<Map<String, Object>> getAuditLogByEncounterId(Long encounterId) {
List<PrescriptionAuditLog> logs = prescriptionAuditLogService.selectByEncounterId(encounterId);
return logs.stream().map(log -> {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", log.getId());
map.put("prescriptionId", log.getPrescriptionId());
map.put("patientName", log.getPatientName());
map.put("doctorName", log.getDoctorName());
map.put("auditResult", log.getAuditResult());
map.put("ruleHit", log.getRuleHit());
map.put("hitCount", log.getHitCount());
map.put("detail", log.getDetail());
map.put("suggestion", log.getSuggestion());
map.put("auditTime", log.getAuditTime());
map.put("createTime", log.getCreateTime());
return map;
}).collect(Collectors.toList());
}
/**
* 配伍禁忌检查 — 检查药品列表中所有两两组合
*/
@Override
public List<InteractionCheckResultDto> checkInteraction(List<String> drugCodes) {
List<InteractionCheckResultDto> results = new ArrayList<>();
if (drugCodes == null || drugCodes.size() < 2) {
return results;
}
for (int i = 0; i < drugCodes.size(); i++) {
for (int j = i + 1; j < drugCodes.size(); j++) {
List<DrugInteractionRule> rules = drugInteractionRuleService
.selectInteractions(drugCodes.get(i), drugCodes.get(j));
if (rules.isEmpty()) {
InteractionCheckResultDto dto = new InteractionCheckResultDto();
dto.setHasInteraction(false);
dto.setDescription("药品 " + drugCodes.get(i) + "" + drugCodes.get(j) + " 未发现配伍禁忌");
results.add(dto);
} else {
for (DrugInteractionRule rule : rules) {
InteractionCheckResultDto dto = new InteractionCheckResultDto();
dto.setHasInteraction(true);
dto.setSeverity(rule.getSeverity());
dto.setDescription(rule.getDrugAName() + "" + rule.getDrugBName() + ": " + rule.getDescription());
dto.setSuggestion(rule.getSuggestion());
results.add(dto);
}
}
}
}
return results;
}
/**
* 剂量检查
*/
@Override
public AuditResultDto checkDosage(String drugCode, BigDecimal dosage, String population) {
LambdaQueryWrapper<DrugDosageRange> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DrugDosageRange::getDrugCode, drugCode)
.eq(DrugDosageRange::getPopulation, population)
.eq(DrugDosageRange::getEnabled, "1")
.eq(DrugDosageRange::getDelFlag, "0")
.last("LIMIT 1");
DrugDosageRange range = drugDosageRangeService.getOne(wrapper);
if (range == null) {
return buildResult("MANUAL", "DOSAGE_NOT_FOUND", 1,
"药品 " + drugCode + " 无对应人群 " + population + " 的剂量规则",
"建议人工确认剂量合理性");
}
if (dosage.compareTo(range.getMinDose()) < 0) {
return buildResult("REJECT", "DOSAGE_BELOW_MIN", 1,
"剂量 " + dosage + range.getDoseUnit() + " 低于最小剂量 " + range.getMinDose() + range.getDoseUnit(),
"建议调整剂量至 " + range.getMinDose() + "-" + range.getMaxDose() + range.getDoseUnit());
}
if (dosage.compareTo(range.getMaxDose()) > 0) {
return buildResult("REJECT", "DOSAGE_ABOVE_MAX", 1,
"剂量 " + dosage + range.getDoseUnit() + " 超过最大剂量 " + range.getMaxDose() + range.getDoseUnit(),
"建议调整剂量至 " + range.getMinDose() + "-" + range.getMaxDose() + range.getDoseUnit());
}
return buildResult("PASS", null, 0, "剂量在合理范围内", null);
}
/**
* 保存审核日志
*/
private void saveAuditLog(PrescriptionAuditDto dto, AuditResultDto result) {
PrescriptionAuditLog logEntity = new PrescriptionAuditLog()
.setPrescriptionId(dto.getPrescriptionId())
.setEncounterId(dto.getEncounterId())
.setPatientId(dto.getPatientId())
.setPatientName(dto.getPatientName())
.setDoctorId(dto.getDoctorId())
.setDoctorName(dto.getDoctorName())
.setAuditResult(result.getAuditResult())
.setAuditType("AUTO")
.setRuleHit(result.getRuleHit())
.setHitCount(result.getHitCount())
.setDetail(result.getDetail())
.setSuggestion(result.getSuggestion())
.setAuditTime(new Date());
prescriptionAuditLogService.save(logEntity);
}
/**
* 构建审核结果DTO
*/
private AuditResultDto buildResult(String auditResult, String ruleHit,
Integer hitCount, String detail, String suggestion) {
AuditResultDto dto = new AuditResultDto();
dto.setAuditResult(auditResult);
dto.setRuleHit(ruleHit);
dto.setHitCount(hitCount);
dto.setDetail(detail);
dto.setSuggestion(suggestion);
return dto;
}
/**
* 合并严重程度取最严重
*/
private String mergeSeverity(String current, String candidate) {
if (current == null) return candidate;
int currentLevel = severityLevel(current);
int candidateLevel = severityLevel(candidate);
return candidateLevel > currentLevel ? candidate : current;
}
private int severityLevel(String severity) {
return switch (severity) {
case "CRITICAL" -> 3;
case "MAJOR" -> 2;
case "MODERATE" -> 1;
default -> 0;
};
}
}

View File

@@ -0,0 +1,130 @@
package com.healthlink.his.web.rationaldrug.controller;
import com.core.common.core.domain.AjaxResult;
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.InteractionCheckResultDto;
import com.healthlink.his.rationaldrug.dto.PrescriptionAuditDto;
import com.healthlink.his.rationaldrug.service.IDrugDosageRangeService;
import com.healthlink.his.rationaldrug.service.IDrugInteractionRuleService;
import com.healthlink.his.web.rationaldrug.appservice.IRationalDrugAppService;
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.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 合理用药管理Controller
*
* @author system
*/
@RestController
@RequestMapping("/healthlink-his/api/v1/rational-drug")
@AllArgsConstructor
@Slf4j
@Tag(name = "合理用药管理")
public class RationalDrugController {
private final IRationalDrugAppService rationalDrugAppService;
private final IDrugInteractionRuleService drugInteractionRuleService;
private final IDrugDosageRangeService drugDosageRangeService;
// ==================== 审核相关接口 ====================
@PostMapping("/audit")
@Operation(summary = "处方审核")
public AjaxResult auditPrescription(@RequestBody PrescriptionAuditDto dto) {
AuditResultDto result = rationalDrugAppService.auditPrescription(dto);
return AjaxResult.success(result);
}
@PostMapping("/batch-audit")
@Operation(summary = "批量审核")
public AjaxResult batchAudit(@RequestBody List<Long> prescriptionIds) {
List<AuditResultDto> results = rationalDrugAppService.batchAudit(prescriptionIds);
return AjaxResult.success(results);
}
@GetMapping("/statistics")
@Operation(summary = "审核统计")
public AjaxResult getAuditStatistics() {
AuditStatisticsDto statistics = rationalDrugAppService.getAuditStatistics();
return AjaxResult.success(statistics);
}
@GetMapping("/trend")
@Operation(summary = "审核趋势")
public AjaxResult getAuditTrend(@RequestParam String startDate) {
List<Map<String, Object>> trend = rationalDrugAppService.getAuditTrend(startDate);
return AjaxResult.success(trend);
}
@GetMapping("/audit-log/{encounterId}")
@Operation(summary = "审核记录")
public AjaxResult getAuditLogByEncounterId(@PathVariable Long encounterId) {
List<Map<String, Object>> logs = rationalDrugAppService.getAuditLogByEncounterId(encounterId);
return AjaxResult.success(logs);
}
// ==================== 配伍禁忌相关接口 ====================
@PostMapping("/check-interaction")
@Operation(summary = "配伍禁忌检查")
public AjaxResult checkInteraction(@RequestBody List<String> drugCodes) {
List<InteractionCheckResultDto> results = rationalDrugAppService.checkInteraction(drugCodes);
return AjaxResult.success(results);
}
@GetMapping("/interaction-rules")
@Operation(summary = "配伍禁忌规则列表")
public AjaxResult listInteractionRules() {
List<DrugInteractionRule> rules = drugInteractionRuleService.list();
return AjaxResult.success(rules);
}
@PostMapping("/interaction-rules")
@Operation(summary = "新增配伍禁忌规则")
public AjaxResult addInteractionRule(@RequestBody DrugInteractionRule rule) {
drugInteractionRuleService.save(rule);
return AjaxResult.success();
}
@PutMapping("/interaction-rules")
@Operation(summary = "修改配伍禁忌规则")
public AjaxResult updateInteractionRule(@RequestBody DrugInteractionRule rule) {
drugInteractionRuleService.updateById(rule);
return AjaxResult.success();
}
@DeleteMapping("/interaction-rules/{id}")
@Operation(summary = "删除配伍禁忌规则")
public AjaxResult deleteInteractionRule(@PathVariable Long id) {
drugInteractionRuleService.removeById(id);
return AjaxResult.success();
}
// ==================== 剂量规则相关接口 ====================
@GetMapping("/dosage-rules")
@Operation(summary = "剂量规则列表")
public AjaxResult listDosageRules() {
List<DrugDosageRange> rules = drugDosageRangeService.list();
return AjaxResult.success(rules);
}
@PostMapping("/check-dosage")
@Operation(summary = "剂量检查")
public AjaxResult checkDosage(@RequestParam String drugCode,
@RequestParam BigDecimal dosage,
@RequestParam String population) {
AuditResultDto result = rationalDrugAppService.checkDosage(drugCode, dosage, population);
return AjaxResult.success(result);
}
}

View File

@@ -0,0 +1,86 @@
-- ============================================================
-- V2: 合理用药系统 — 配伍禁忌规则 + 处方审核日志
-- ============================================================
-- 配伍禁忌规则表
CREATE TABLE drug_interaction_rule (
id BIGSERIAL PRIMARY KEY,
drug_a_code VARCHAR(32) NOT NULL,
drug_a_name VARCHAR(128),
drug_b_code VARCHAR(32) NOT NULL,
drug_b_name VARCHAR(128),
severity VARCHAR(16) NOT NULL DEFAULT 'MODERATE',
description TEXT,
suggestion TEXT,
enabled CHAR(1) DEFAULT '1',
del_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
tenant_id VARCHAR(20) DEFAULT '0'
);
COMMENT ON TABLE drug_interaction_rule IS '配伍禁忌规则表';
COMMENT ON COLUMN drug_interaction_rule.severity IS '严重程度: CRITICAL-禁忌 MAJOR-严重 MODERATE-一般';
CREATE INDEX idx_drug_interaction_a ON drug_interaction_rule(drug_a_code);
CREATE INDEX idx_drug_interaction_b ON drug_interaction_rule(drug_b_code);
-- 处方审核日志表
CREATE TABLE prescription_audit_log (
id BIGSERIAL PRIMARY KEY,
prescription_id BIGINT NOT NULL,
encounter_id BIGINT NOT NULL,
patient_id BIGINT NOT NULL,
patient_name VARCHAR(64),
doctor_id BIGINT NOT NULL,
doctor_name VARCHAR(64),
audit_result VARCHAR(16) NOT NULL DEFAULT 'PENDING',
audit_type VARCHAR(32) NOT NULL DEFAULT 'AUTO',
rule_hit VARCHAR(128),
hit_count INTEGER DEFAULT 0,
detail TEXT,
suggestion TEXT,
auditor_id BIGINT,
auditor_name VARCHAR(64),
audit_time TIMESTAMP,
del_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
tenant_id VARCHAR(20) DEFAULT '0'
);
COMMENT ON TABLE prescription_audit_log IS '处方审核日志表';
COMMENT ON COLUMN prescription_audit_log.audit_result IS '审核结果: PASS-通过 REJECT-拒绝 MANUAL-待人工审核 PENDING-待审核';
COMMENT ON COLUMN prescription_audit_log.audit_type IS '审核类型: AUTO-自动审核 MANUAL-人工审核';
CREATE INDEX idx_audit_log_prescription ON prescription_audit_log(prescription_id);
CREATE INDEX idx_audit_log_patient ON prescription_audit_log(patient_id);
CREATE INDEX idx_audit_log_encounter ON prescription_audit_log(encounter_id);
CREATE INDEX idx_audit_log_result ON prescription_audit_log(audit_result);
-- 剂量范围规则表
CREATE TABLE drug_dosage_range (
id BIGSERIAL PRIMARY KEY,
drug_code VARCHAR(32) NOT NULL,
drug_name VARCHAR(128),
population VARCHAR(32) NOT NULL DEFAULT 'ADULT',
min_dose DECIMAL(10,2),
max_dose DECIMAL(10,2),
dose_unit VARCHAR(16),
frequency_min INTEGER,
frequency_max INTEGER,
route VARCHAR(32),
organ_function VARCHAR(32),
adjustment_note TEXT,
enabled CHAR(1) DEFAULT '1',
del_flag CHAR(1) DEFAULT '0',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
tenant_id VARCHAR(20) DEFAULT '0'
);
COMMENT ON TABLE drug_dosage_range IS '剂量范围规则表';
COMMENT ON COLUMN drug_dosage_range.population IS '人群: ADULT-成人 CHILD-儿童 ELDERLY-老年 PREGNANT-孕妇';
COMMENT ON COLUMN drug_dosage_range.organ_function IS '器官功能: NORMAL-正常 RENAL-肾功能不全 HEPATIC-肝功能不全';
CREATE INDEX idx_dosage_range_drug ON drug_dosage_range(drug_code);

View File

@@ -0,0 +1,103 @@
-- ============================================================
-- V3: 麻醉记录系统
-- ============================================================
-- 麻醉记录主表
CREATE TABLE anes_record (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT NOT NULL,
surgery_id BIGINT,
patient_id BIGINT NOT NULL,
patient_name VARCHAR(64),
anesthetist_id BIGINT,
anesthetist_name VARCHAR(64),
asa_grade VARCHAR(8),
anesthesia_type VARCHAR(32),
anesthesia_method VARCHAR(64),
start_time TIMESTAMP,
end_time TIMESTAMP,
duration_minutes INTEGER,
airway_assessment TEXT,
fasting_confirmed BOOLEAN DEFAULT FALSE,
consent_signed BOOLEAN DEFAULT FALSE,
summary TEXT,
complications TEXT,
status VARCHAR(16) DEFAULT 'DRAFT',
del_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
tenant_id VARCHAR(20) DEFAULT '0'
);
COMMENT ON TABLE anes_record IS '麻醉记录主表';
-- 麻醉生命体征记录表(5分钟间隔)
CREATE TABLE anes_vital_sign (
id BIGSERIAL PRIMARY KEY,
record_id BIGINT NOT NULL,
record_time TIMESTAMP NOT NULL,
heart_rate INTEGER,
blood_pressure_sys INTEGER,
blood_pressure_dia INTEGER,
mean_arterial_pressure INTEGER,
spo2 DECIMAL(5,2),
etco2 DECIMAL(5,2),
temperature DECIMAL(4,1),
respiratory_rate INTEGER,
tidal_volume INTEGER,
anesthesia_depth VARCHAR(16),
remark TEXT,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE anes_vital_sign IS '麻醉生命体征记录';
-- 麻醉用药记录表
CREATE TABLE anes_medication (
id BIGSERIAL PRIMARY KEY,
record_id BIGINT NOT NULL,
drug_code VARCHAR(32),
drug_name VARCHAR(128) NOT NULL,
dosage VARCHAR(64),
dose_unit VARCHAR(16),
route VARCHAR(32),
start_time TIMESTAMP,
end_time TIMESTAMP,
frequency VARCHAR(32),
operator_id BIGINT,
remark TEXT,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE anes_medication IS '麻醉用药记录';
-- 麻醉出入量记录表
CREATE TABLE anes_io_record (
id BIGSERIAL PRIMARY KEY,
record_id BIGINT NOT NULL,
record_type VARCHAR(16) NOT NULL,
item_name VARCHAR(64),
amount DECIMAL(10,2),
unit VARCHAR(16),
record_time TIMESTAMP,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE anes_io_record IS '麻醉出入量记录';
COMMENT ON COLUMN anes_io_record.record_type IS '类型: INPUT-输入 OUTPUT-输出';
-- 麻醉术后随访表
CREATE TABLE anes_postoperative_followup (
id BIGSERIAL PRIMARY KEY,
record_id BIGINT NOT NULL,
followup_date DATE,
followup_time TIMESTAMP,
pain_score INTEGER,
nausea_vomiting BOOLEAN DEFAULT FALSE,
complications TEXT,
paca_score INTEGER,
followup_by BIGINT,
followup_by_name VARCHAR(64),
notes TEXT,
del_flag CHAR(1) DEFAULT '0',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE anes_postoperative_followup IS '麻醉术后随访';

View File

@@ -0,0 +1,59 @@
-- ============================================================
-- V4: 病案首页管理
-- ============================================================
-- 病案首页主表
CREATE TABLE mr_homepage (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT NOT NULL UNIQUE,
patient_id BIGINT NOT NULL,
admission_date DATE,
discharge_date DATE,
los_days INTEGER,
primary_diagnosis_code VARCHAR(16),
primary_diagnosis_name VARCHAR(128),
other_diagnosis_codes TEXT,
other_diagnosis_names TEXT,
primary_procedure_code VARCHAR(16),
primary_procedure_name VARCHAR(128),
other_procedure_codes TEXT,
other_procedure_names TEXT,
admission_condition VARCHAR(32),
discharge_condition VARCHAR(32),
discharge_destination VARCHAR(32),
drg_group VARCHAR(32),
drg_weight DECIMAL(8,4),
total_cost DECIMAL(12,2) DEFAULT 0,
self_pay_cost DECIMAL(12,2) DEFAULT 0,
insurance_cost DECIMAL(12,2) DEFAULT 0,
drug_cost DECIMAL(12,2) DEFAULT 0,
examination_cost DECIMAL(12,2) DEFAULT 0,
lab_cost DECIMAL(12,2) DEFAULT 0,
treatment_cost DECIMAL(12,2) DEFAULT 0,
material_cost DECIMAL(12,2) DEFAULT 0,
quality_status VARCHAR(16) DEFAULT 'DRAFT',
quality_score DECIMAL(5,2),
submit_time TIMESTAMP,
del_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
tenant_id VARCHAR(20) DEFAULT '0'
);
COMMENT ON TABLE mr_homepage IS '病案首页';
-- 病案首页质控记录表
CREATE TABLE mr_homepage_quality_check (
id BIGSERIAL PRIMARY KEY,
homepage_id BIGINT NOT NULL,
check_item VARCHAR(64) NOT NULL,
check_category VARCHAR(32),
check_result VARCHAR(16) NOT NULL,
check_detail TEXT,
suggestion TEXT,
checker VARCHAR(32),
check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE mr_homepage_quality_check IS '病案首页质控记录';

View File

@@ -0,0 +1,46 @@
package com.healthlink.his.anesthesia.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
@Data
@TableName("anes_postoperative_followup")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class AnesthesiaFollowup extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long recordId;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date followupDate;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date followupTime;
private Integer painScore;
private Boolean nauseaVomiting;
private String complications;
private Integer pacaScore;
private Long followupBy;
private String followupByName;
private String notes;
private String delFlag;
}

View File

@@ -0,0 +1,36 @@
package com.healthlink.his.anesthesia.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("anes_io_record")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class AnesthesiaIoRecord extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long recordId;
private String recordType;
private String itemName;
private BigDecimal amount;
private String unit;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date recordTime;
}

View File

@@ -0,0 +1,46 @@
package com.healthlink.his.anesthesia.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
@Data
@TableName("anes_medication")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class AnesthesiaMedication extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long recordId;
private String drugCode;
private String drugName;
private String dosage;
private String doseUnit;
private String route;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
private String frequency;
private Long operatorId;
private String remark;
}

View File

@@ -0,0 +1,62 @@
package com.healthlink.his.anesthesia.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
@Data
@TableName("anes_record")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class AnesthesiaRecord extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long encounterId;
private Long surgeryId;
private Long patientId;
private String patientName;
private Long anesthetistId;
private String anesthetistName;
private String asaGrade;
private String anesthesiaType;
private String anesthesiaMethod;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
private Integer durationMinutes;
private String airwayAssessment;
private Boolean fastingConfirmed;
private Boolean consentSigned;
private String summary;
private String complications;
private String status;
private String delFlag;
}

View File

@@ -0,0 +1,50 @@
package com.healthlink.his.anesthesia.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("anes_vital_sign")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class AnesthesiaVitalSign extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long recordId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date recordTime;
private Integer heartRate;
private Integer bloodPressureSys;
private Integer bloodPressureDia;
private Integer meanArterialPressure;
private BigDecimal spo2;
private BigDecimal etco2;
private BigDecimal temperature;
private Integer respiratoryRate;
private Integer tidalVolume;
private String anesthesiaDepth;
private String remark;
}

View File

@@ -0,0 +1,17 @@
package com.healthlink.his.anesthesia.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
@Data
@Accessors(chain = true)
public class AnesthesiaIoSummaryDto {
private BigDecimal totalInput;
private BigDecimal totalOutput;
private BigDecimal balance;
}

View File

@@ -0,0 +1,26 @@
package com.healthlink.his.anesthesia.dto;
import com.healthlink.his.anesthesia.domain.AnesthesiaFollowup;
import com.healthlink.his.anesthesia.domain.AnesthesiaIoRecord;
import com.healthlink.his.anesthesia.domain.AnesthesiaMedication;
import com.healthlink.his.anesthesia.domain.AnesthesiaRecord;
import com.healthlink.his.anesthesia.domain.AnesthesiaVitalSign;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
@Data
@Accessors(chain = true)
public class AnesthesiaRecordDetailDto {
private AnesthesiaRecord record;
private List<AnesthesiaVitalSign> vitalSigns;
private List<AnesthesiaMedication> medications;
private List<AnesthesiaIoRecord> ioRecords;
private List<AnesthesiaFollowup> followups;
}

View File

@@ -0,0 +1,14 @@
package com.healthlink.his.anesthesia.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.anesthesia.domain.AnesthesiaFollowup;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface AnesthesiaFollowupMapper extends BaseMapper<AnesthesiaFollowup> {
List<AnesthesiaFollowup> selectByRecordId(@Param("recordId") Long recordId);
}

View File

@@ -0,0 +1,19 @@
package com.healthlink.his.anesthesia.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.anesthesia.domain.AnesthesiaIoRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
@Mapper
public interface AnesthesiaIoRecordMapper extends BaseMapper<AnesthesiaIoRecord> {
List<AnesthesiaIoRecord> selectByRecordId(@Param("recordId") Long recordId);
BigDecimal calculateTotalInput(@Param("recordId") Long recordId);
BigDecimal calculateTotalOutput(@Param("recordId") Long recordId);
}

View File

@@ -0,0 +1,14 @@
package com.healthlink.his.anesthesia.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.anesthesia.domain.AnesthesiaMedication;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface AnesthesiaMedicationMapper extends BaseMapper<AnesthesiaMedication> {
List<AnesthesiaMedication> selectByRecordId(@Param("recordId") Long recordId);
}

View File

@@ -0,0 +1,16 @@
package com.healthlink.his.anesthesia.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.anesthesia.domain.AnesthesiaRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface AnesthesiaRecordMapper extends BaseMapper<AnesthesiaRecord> {
List<AnesthesiaRecord> selectByEncounterId(@Param("encounterId") Long encounterId);
List<AnesthesiaRecord> selectByPatientId(@Param("patientId") Long patientId);
}

View File

@@ -0,0 +1,20 @@
package com.healthlink.his.anesthesia.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.anesthesia.domain.AnesthesiaVitalSign;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
@Mapper
public interface AnesthesiaVitalSignMapper extends BaseMapper<AnesthesiaVitalSign> {
List<AnesthesiaVitalSign> selectByRecordId(@Param("recordId") Long recordId);
List<AnesthesiaVitalSign> selectByRecordIdAndTimeRange(
@Param("recordId") Long recordId,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime);
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.anesthesia.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.anesthesia.domain.AnesthesiaFollowup;
import java.util.List;
public interface IAnesthesiaFollowupService extends IService<AnesthesiaFollowup> {
List<AnesthesiaFollowup> selectByRecordId(Long recordId);
}

View File

@@ -0,0 +1,16 @@
package com.healthlink.his.anesthesia.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.anesthesia.domain.AnesthesiaIoRecord;
import java.math.BigDecimal;
import java.util.List;
public interface IAnesthesiaIoRecordService extends IService<AnesthesiaIoRecord> {
List<AnesthesiaIoRecord> selectByRecordId(Long recordId);
BigDecimal calculateTotalInput(Long recordId);
BigDecimal calculateTotalOutput(Long recordId);
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.anesthesia.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.anesthesia.domain.AnesthesiaMedication;
import java.util.List;
public interface IAnesthesiaMedicationService extends IService<AnesthesiaMedication> {
List<AnesthesiaMedication> selectByRecordId(Long recordId);
}

View File

@@ -0,0 +1,13 @@
package com.healthlink.his.anesthesia.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.anesthesia.domain.AnesthesiaRecord;
import java.util.List;
public interface IAnesthesiaRecordService extends IService<AnesthesiaRecord> {
List<AnesthesiaRecord> selectByEncounterId(Long encounterId);
List<AnesthesiaRecord> selectByPatientId(Long patientId);
}

View File

@@ -0,0 +1,14 @@
package com.healthlink.his.anesthesia.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.anesthesia.domain.AnesthesiaVitalSign;
import java.util.Date;
import java.util.List;
public interface IAnesthesiaVitalSignService extends IService<AnesthesiaVitalSign> {
List<AnesthesiaVitalSign> selectByRecordId(Long recordId);
List<AnesthesiaVitalSign> selectByRecordIdAndTimeRange(Long recordId, Date startTime, Date endTime);
}

View File

@@ -0,0 +1,20 @@
package com.healthlink.his.anesthesia.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.anesthesia.domain.AnesthesiaFollowup;
import com.healthlink.his.anesthesia.mapper.AnesthesiaFollowupMapper;
import com.healthlink.his.anesthesia.service.IAnesthesiaFollowupService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class AnesthesiaFollowupServiceImpl
extends ServiceImpl<AnesthesiaFollowupMapper, AnesthesiaFollowup>
implements IAnesthesiaFollowupService {
@Override
public List<AnesthesiaFollowup> selectByRecordId(Long recordId) {
return baseMapper.selectByRecordId(recordId);
}
}

View File

@@ -0,0 +1,31 @@
package com.healthlink.his.anesthesia.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.anesthesia.domain.AnesthesiaIoRecord;
import com.healthlink.his.anesthesia.mapper.AnesthesiaIoRecordMapper;
import com.healthlink.his.anesthesia.service.IAnesthesiaIoRecordService;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
@Service
public class AnesthesiaIoRecordServiceImpl
extends ServiceImpl<AnesthesiaIoRecordMapper, AnesthesiaIoRecord>
implements IAnesthesiaIoRecordService {
@Override
public List<AnesthesiaIoRecord> selectByRecordId(Long recordId) {
return baseMapper.selectByRecordId(recordId);
}
@Override
public BigDecimal calculateTotalInput(Long recordId) {
return baseMapper.calculateTotalInput(recordId);
}
@Override
public BigDecimal calculateTotalOutput(Long recordId) {
return baseMapper.calculateTotalOutput(recordId);
}
}

View File

@@ -0,0 +1,20 @@
package com.healthlink.his.anesthesia.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.anesthesia.domain.AnesthesiaMedication;
import com.healthlink.his.anesthesia.mapper.AnesthesiaMedicationMapper;
import com.healthlink.his.anesthesia.service.IAnesthesiaMedicationService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class AnesthesiaMedicationServiceImpl
extends ServiceImpl<AnesthesiaMedicationMapper, AnesthesiaMedication>
implements IAnesthesiaMedicationService {
@Override
public List<AnesthesiaMedication> selectByRecordId(Long recordId) {
return baseMapper.selectByRecordId(recordId);
}
}

View File

@@ -0,0 +1,25 @@
package com.healthlink.his.anesthesia.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.anesthesia.domain.AnesthesiaRecord;
import com.healthlink.his.anesthesia.mapper.AnesthesiaRecordMapper;
import com.healthlink.his.anesthesia.service.IAnesthesiaRecordService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class AnesthesiaRecordServiceImpl
extends ServiceImpl<AnesthesiaRecordMapper, AnesthesiaRecord>
implements IAnesthesiaRecordService {
@Override
public List<AnesthesiaRecord> selectByEncounterId(Long encounterId) {
return baseMapper.selectByEncounterId(encounterId);
}
@Override
public List<AnesthesiaRecord> selectByPatientId(Long patientId) {
return baseMapper.selectByPatientId(patientId);
}
}

View File

@@ -0,0 +1,26 @@
package com.healthlink.his.anesthesia.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.anesthesia.domain.AnesthesiaVitalSign;
import com.healthlink.his.anesthesia.mapper.AnesthesiaVitalSignMapper;
import com.healthlink.his.anesthesia.service.IAnesthesiaVitalSignService;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Service
public class AnesthesiaVitalSignServiceImpl
extends ServiceImpl<AnesthesiaVitalSignMapper, AnesthesiaVitalSign>
implements IAnesthesiaVitalSignService {
@Override
public List<AnesthesiaVitalSign> selectByRecordId(Long recordId) {
return baseMapper.selectByRecordId(recordId);
}
@Override
public List<AnesthesiaVitalSign> selectByRecordIdAndTimeRange(Long recordId, Date startTime, Date endTime) {
return baseMapper.selectByRecordIdAndTimeRange(recordId, startTime, endTime);
}
}

View File

@@ -0,0 +1,86 @@
package com.healthlink.his.mrhomepage.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("mr_homepage")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class MrHomepage extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long encounterId;
private Long patientId;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date admissionDate;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date dischargeDate;
private Integer losDays;
private String primaryDiagnosisCode;
private String primaryDiagnosisName;
private String otherDiagnosisCodes;
private String otherDiagnosisNames;
private String primaryProcedureCode;
private String primaryProcedureName;
private String otherProcedureCodes;
private String otherProcedureNames;
private String admissionCondition;
private String dischargeCondition;
private String dischargeDestination;
private String drgGroup;
private BigDecimal drgWeight;
private BigDecimal totalCost;
private BigDecimal selfPayCost;
private BigDecimal insuranceCost;
private BigDecimal drugCost;
private BigDecimal examinationCost;
private BigDecimal labCost;
private BigDecimal treatmentCost;
private BigDecimal materialCost;
private String qualityStatus;
private BigDecimal qualityScore;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date submitTime;
private String delFlag;
}

View File

@@ -0,0 +1,39 @@
package com.healthlink.his.mrhomepage.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
@Data
@TableName("mr_homepage_quality_check")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class MrHomepageQualityCheck extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long homepageId;
private String checkItem;
private String checkCategory;
private String checkResult;
private String checkDetail;
private String suggestion;
private String checker;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date checkTime;
}

View File

@@ -0,0 +1,16 @@
package com.healthlink.his.mrhomepage.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.mrhomepage.domain.MrHomepage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface MrHomepageMapper extends BaseMapper<MrHomepage> {
List<MrHomepage> selectByEncounterId(@Param("encounterId") Long encounterId);
List<MrHomepage> selectByDischargeDate(@Param("startDate") String startDate, @Param("endDate") String endDate);
}

View File

@@ -0,0 +1,14 @@
package com.healthlink.his.mrhomepage.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.mrhomepage.domain.MrHomepageQualityCheck;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface MrHomepageQualityCheckMapper extends BaseMapper<MrHomepageQualityCheck> {
List<MrHomepageQualityCheck> selectByHomepageId(@Param("homepageId") Long homepageId);
}

View File

@@ -0,0 +1,13 @@
package com.healthlink.his.mrhomepage.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.mrhomepage.domain.MrHomepageQualityCheck;
import java.util.List;
public interface IMrHomepageQualityCheckService extends IService<MrHomepageQualityCheck> {
List<MrHomepageQualityCheck> selectByHomepageId(Long homepageId);
List<MrHomepageQualityCheck> executeAutoCheck(Long homepageId);
}

View File

@@ -0,0 +1,16 @@
package com.healthlink.his.mrhomepage.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.mrhomepage.domain.MrHomepage;
import java.util.List;
import java.util.Map;
public interface IMrHomepageService extends IService<MrHomepage> {
List<MrHomepage> selectByEncounterId(Long encounterId);
List<MrHomepage> selectByDischargeDate(String startDate, String endDate);
Map<String, Object> selectStatistics(String startDate, String endDate);
}

View File

@@ -0,0 +1,77 @@
package com.healthlink.his.mrhomepage.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.mrhomepage.domain.MrHomepage;
import com.healthlink.his.mrhomepage.domain.MrHomepageQualityCheck;
import com.healthlink.his.mrhomepage.mapper.MrHomepageQualityCheckMapper;
import com.healthlink.his.mrhomepage.service.IMrHomepageQualityCheckService;
import com.healthlink.his.mrhomepage.service.IMrHomepageService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
public class MrHomepageQualityCheckServiceImpl
extends ServiceImpl<MrHomepageQualityCheckMapper, MrHomepageQualityCheck>
implements IMrHomepageQualityCheckService {
@Resource
private IMrHomepageService mrHomepageService;
@Override
public List<MrHomepageQualityCheck> selectByHomepageId(Long homepageId) {
return baseMapper.selectByHomepageId(homepageId);
}
@Override
@Transactional
public List<MrHomepageQualityCheck> executeAutoCheck(Long homepageId) {
MrHomepage homepage = mrHomepageService.getById(homepageId);
List<MrHomepageQualityCheck> checks = new ArrayList<>();
// 主诊断编码检查
MrHomepageQualityCheck diagnosisCheck = new MrHomepageQualityCheck()
.setHomepageId(homepageId)
.setCheckItem("主诊断编码完整性")
.setCheckCategory("基础信息")
.setCheckResult(homepage.getPrimaryDiagnosisCode() != null ? "PASS" : "FAIL")
.setCheckDetail("主诊断编码: " + homepage.getPrimaryDiagnosisCode())
.setSuggestion(homepage.getPrimaryDiagnosisCode() == null ? "请填写主诊断编码" : null)
.setChecker("AUTO")
.setCheckTime(new Date());
save(diagnosisCheck);
checks.add(diagnosisCheck);
// 主手术编码检查
MrHomepageQualityCheck procedureCheck = new MrHomepageQualityCheck()
.setHomepageId(homepageId)
.setCheckItem("主手术编码完整性")
.setCheckCategory("基础信息")
.setCheckResult(homepage.getPrimaryProcedureCode() != null ? "PASS" : "FAIL")
.setCheckDetail("主手术编码: " + homepage.getPrimaryProcedureCode())
.setSuggestion(homepage.getPrimaryProcedureCode() == null ? "请填写主手术编码" : null)
.setChecker("AUTO")
.setCheckTime(new Date());
save(procedureCheck);
checks.add(procedureCheck);
// 入院日期检查
MrHomepageQualityCheck admissionCheck = new MrHomepageQualityCheck()
.setHomepageId(homepageId)
.setCheckItem("入院日期完整性")
.setCheckCategory("日期信息")
.setCheckResult(homepage.getAdmissionDate() != null ? "PASS" : "FAIL")
.setCheckDetail("入院日期: " + homepage.getAdmissionDate())
.setSuggestion(homepage.getAdmissionDate() == null ? "请填写入院日期" : null)
.setChecker("AUTO")
.setCheckTime(new Date());
save(admissionCheck);
checks.add(admissionCheck);
return checks;
}
}

View File

@@ -0,0 +1,44 @@
package com.healthlink.his.mrhomepage.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.mrhomepage.domain.MrHomepage;
import com.healthlink.his.mrhomepage.mapper.MrHomepageMapper;
import com.healthlink.his.mrhomepage.service.IMrHomepageService;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class MrHomepageServiceImpl
extends ServiceImpl<MrHomepageMapper, MrHomepage>
implements IMrHomepageService {
@Override
public List<MrHomepage> selectByEncounterId(Long encounterId) {
return baseMapper.selectByEncounterId(encounterId);
}
@Override
public List<MrHomepage> selectByDischargeDate(String startDate, String endDate) {
return baseMapper.selectByDischargeDate(startDate, endDate);
}
@Override
public Map<String, Object> selectStatistics(String startDate, String endDate) {
List<MrHomepage> list = baseMapper.selectByDischargeDate(startDate, endDate);
Map<String, Object> stats = new HashMap<>();
stats.put("totalCount", list.size());
stats.put("totalCost", list.stream()
.map(MrHomepage::getTotalCost)
.reduce(BigDecimal.ZERO, BigDecimal::add));
stats.put("avgCost", list.isEmpty() ? BigDecimal.ZERO
: stats.get("totalCost").toString().equals("0") ? BigDecimal.ZERO
: new BigDecimal(stats.get("totalCost").toString())
.divide(BigDecimal.valueOf(list.size()), 2, BigDecimal.ROUND_HALF_UP));
return stats;
}
}

View File

@@ -0,0 +1,46 @@
package com.healthlink.his.rationaldrug.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
@Data
@TableName("drug_dosage_range")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class DrugDosageRange extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String drugCode;
private String drugName;
private String population;
private BigDecimal minDose;
private BigDecimal maxDose;
private String doseUnit;
private Integer frequencyMin;
private Integer frequencyMax;
private String route;
private String organFunction;
private String adjustmentNote;
private String enabled;
private String delFlag;
}

View File

@@ -0,0 +1,37 @@
package com.healthlink.his.rationaldrug.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@Data
@TableName("drug_interaction_rule")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class DrugInteractionRule extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String drugACode;
private String drugAName;
private String drugBCode;
private String drugBName;
private String severity;
private String description;
private String suggestion;
private String enabled;
private String delFlag;
}

View File

@@ -0,0 +1,52 @@
package com.healthlink.his.rationaldrug.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
@Data
@TableName("prescription_audit_log")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class PrescriptionAuditLog extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long prescriptionId;
private Long encounterId;
private Long patientId;
private String patientName;
private Long doctorId;
private String doctorName;
private String auditResult;
private String auditType;
private String ruleHit;
private Integer hitCount;
private String detail;
private String suggestion;
private Long auditorId;
private String auditorName;
private Date auditTime;
private String delFlag;
}

View File

@@ -0,0 +1,29 @@
package com.healthlink.his.rationaldrug.dto;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 审核结果DTO
*
* @author system
*/
@Data
@Accessors(chain = true)
public class AuditResultDto {
/** 审核结果: PASS/REJECT/MANUAL */
private String auditResult;
/** 命中的规则 */
private String ruleHit;
/** 命中数量 */
private Integer hitCount;
/** 详细信息 */
private String detail;
/** 处置建议 */
private String suggestion;
}

View File

@@ -0,0 +1,31 @@
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 AuditStatisticsDto {
/** 总审核数 */
private Long total;
/** 通过数 */
private Long passCount;
/** 拒绝数 */
private Long rejectCount;
/** 待人工审核数 */
private Long manualCount;
/** 通过率 */
private BigDecimal passRate;
}

View File

@@ -0,0 +1,26 @@
package com.healthlink.his.rationaldrug.dto;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 配伍禁忌检查结果DTO
*
* @author system
*/
@Data
@Accessors(chain = true)
public class InteractionCheckResultDto {
/** 是否存在配伍禁忌 */
private Boolean hasInteraction;
/** 严重程度: CRITICAL/MAJOR/MODERATE */
private String severity;
/** 描述 */
private String description;
/** 处置建议 */
private String suggestion;
}

View File

@@ -0,0 +1,40 @@
package com.healthlink.his.rationaldrug.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/**
* 处方审核请求DTO
*
* @author system
*/
@Data
@Accessors(chain = true)
public class PrescriptionAuditDto {
/** 处方ID */
private Long prescriptionId;
/** 就诊ID */
private Long encounterId;
/** 患者ID */
private Long patientId;
/** 医生ID */
private Long doctorId;
/** 患者姓名 */
private String patientName;
/** 医生姓名 */
private String doctorName;
/** 药品编码列表 */
private List<String> drugCodes;
/** 人群类型: ADULT/CHILD/ELDERLY/PREGNANT */
private String population;
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.rationaldrug.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.rationaldrug.domain.DrugDosageRange;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DrugDosageRangeMapper extends BaseMapper<DrugDosageRange> {
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.rationaldrug.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.rationaldrug.domain.DrugInteractionRule;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DrugInteractionRuleMapper extends BaseMapper<DrugInteractionRule> {
}

View File

@@ -0,0 +1,21 @@
package com.healthlink.his.rationaldrug.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.rationaldrug.domain.PrescriptionAuditLog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.Map;
@Mapper
public interface PrescriptionAuditLogMapper extends BaseMapper<PrescriptionAuditLog> {
@Select("SELECT audit_result, COUNT(*) as count FROM prescription_audit_log WHERE del_flag='0' GROUP BY audit_result")
java.util.List<Map<String, Object>> selectAuditStatistics();
@Select("SELECT DATE_TRUNC('day', create_time) as audit_date, COUNT(*) as total, " +
"SUM(CASE WHEN audit_result='PASS' THEN 1 ELSE 0 END) as pass_count " +
"FROM prescription_audit_log WHERE del_flag='0' AND create_time >= #{startDate} " +
"GROUP BY DATE_TRUNC('day', create_time) ORDER BY audit_date")
java.util.List<Map<String, Object>> selectAuditTrend(@Param("startDate") String startDate);
}

View File

@@ -0,0 +1,12 @@
package com.healthlink.his.rationaldrug.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.rationaldrug.domain.DrugDosageRange;
/**
* 剂量范围规则Service接口
*
* @author system
*/
public interface IDrugDosageRangeService extends IService<DrugDosageRange> {
}

View File

@@ -0,0 +1,31 @@
package com.healthlink.his.rationaldrug.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.rationaldrug.domain.DrugInteractionRule;
import java.util.List;
/**
* 配伍禁忌规则Service接口
*
* @author system
*/
public interface IDrugInteractionRuleService extends IService<DrugInteractionRule> {
/**
* 根据药品编码查询相关配伍禁忌规则
*
* @param drugCode 药品编码
* @return 配伍禁忌规则列表
*/
List<DrugInteractionRule> selectByDrugCode(String drugCode);
/**
* 查询两种药品之间的配伍禁忌
*
* @param drugA 药品A编码
* @param drugB 药品B编码
* @return 配伍禁忌规则列表
*/
List<DrugInteractionRule> selectInteractions(String drugA, String drugB);
}

View File

@@ -0,0 +1,38 @@
package com.healthlink.his.rationaldrug.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.rationaldrug.domain.PrescriptionAuditLog;
import java.util.List;
import java.util.Map;
/**
* 处方审核日志Service接口
*
* @author system
*/
public interface IPrescriptionAuditLogService extends IService<PrescriptionAuditLog> {
/**
* 查询审核统计数据
*
* @return 审核统计结果 (audit_result -> count)
*/
List<Map<String, Object>> selectAuditStatistics();
/**
* 查询审核趋势数据
*
* @param startDate 起始日期
* @return 审核趋势结果
*/
List<Map<String, Object>> selectAuditTrend(String startDate);
/**
* 根据就诊ID查询审核记录
*
* @param encounterId 就诊ID
* @return 审核记录列表
*/
List<PrescriptionAuditLog> selectByEncounterId(Long encounterId);
}

View File

@@ -0,0 +1,18 @@
package com.healthlink.his.rationaldrug.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.rationaldrug.domain.DrugDosageRange;
import com.healthlink.his.rationaldrug.mapper.DrugDosageRangeMapper;
import com.healthlink.his.rationaldrug.service.IDrugDosageRangeService;
import org.springframework.stereotype.Service;
/**
* 剂量范围规则Service业务层处理
*
* @author system
*/
@Service
public class DrugDosageRangeServiceImpl
extends ServiceImpl<DrugDosageRangeMapper, DrugDosageRange>
implements IDrugDosageRangeService {
}

View File

@@ -0,0 +1,59 @@
package com.healthlink.his.rationaldrug.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.rationaldrug.domain.DrugInteractionRule;
import com.healthlink.his.rationaldrug.mapper.DrugInteractionRuleMapper;
import com.healthlink.his.rationaldrug.service.IDrugInteractionRuleService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 配伍禁忌规则Service业务层处理
*
* @author system
*/
@Service
public class DrugInteractionRuleServiceImpl
extends ServiceImpl<DrugInteractionRuleMapper, DrugInteractionRule>
implements IDrugInteractionRuleService {
/**
* 根据药品编码查询相关配伍禁忌规则
*
* @param drugCode 药品编码
* @return 配伍禁忌规则列表
*/
@Override
public List<DrugInteractionRule> selectByDrugCode(String drugCode) {
LambdaQueryWrapper<DrugInteractionRule> wrapper = new LambdaQueryWrapper<>();
wrapper.and(w -> w.eq(DrugInteractionRule::getDrugACode, drugCode)
.or()
.eq(DrugInteractionRule::getDrugBCode, drugCode))
.eq(DrugInteractionRule::getEnabled, "1")
.eq(DrugInteractionRule::getDelFlag, "0");
return baseMapper.selectList(wrapper);
}
/**
* 查询两种药品之间的配伍禁忌
*
* @param drugA 药品A编码
* @param drugB 药品B编码
* @return 配伍禁忌规则列表
*/
@Override
public List<DrugInteractionRule> selectInteractions(String drugA, String drugB) {
LambdaQueryWrapper<DrugInteractionRule> wrapper = new LambdaQueryWrapper<>();
wrapper.and(w -> w
.and(inner -> inner.eq(DrugInteractionRule::getDrugACode, drugA)
.eq(DrugInteractionRule::getDrugBCode, drugB))
.or()
.and(inner -> inner.eq(DrugInteractionRule::getDrugACode, drugB)
.eq(DrugInteractionRule::getDrugBCode, drugA)))
.eq(DrugInteractionRule::getEnabled, "1")
.eq(DrugInteractionRule::getDelFlag, "0");
return baseMapper.selectList(wrapper);
}
}

View File

@@ -0,0 +1,58 @@
package com.healthlink.his.rationaldrug.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.rationaldrug.domain.PrescriptionAuditLog;
import com.healthlink.his.rationaldrug.mapper.PrescriptionAuditLogMapper;
import com.healthlink.his.rationaldrug.service.IPrescriptionAuditLogService;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* 处方审核日志Service业务层处理
*
* @author system
*/
@Service
public class PrescriptionAuditLogServiceImpl
extends ServiceImpl<PrescriptionAuditLogMapper, PrescriptionAuditLog>
implements IPrescriptionAuditLogService {
/**
* 查询审核统计数据
*
* @return 审核统计结果
*/
@Override
public List<Map<String, Object>> selectAuditStatistics() {
return baseMapper.selectAuditStatistics();
}
/**
* 查询审核趋势数据
*
* @param startDate 起始日期
* @return 审核趋势结果
*/
@Override
public List<Map<String, Object>> selectAuditTrend(String startDate) {
return baseMapper.selectAuditTrend(startDate);
}
/**
* 根据就诊ID查询审核记录
*
* @param encounterId 就诊ID
* @return 审核记录列表
*/
@Override
public List<PrescriptionAuditLog> selectByEncounterId(Long encounterId) {
LambdaQueryWrapper<PrescriptionAuditLog> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PrescriptionAuditLog::getEncounterId, encounterId)
.eq(PrescriptionAuditLog::getDelFlag, "0")
.orderByDesc(PrescriptionAuditLog::getCreateTime);
return baseMapper.selectList(wrapper);
}
}

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.healthlink.his.anesthesia.mapper.AnesthesiaFollowupMapper">
<select id="selectByRecordId" resultType="com.healthlink.his.anesthesia.domain.AnesthesiaFollowup">
SELECT * FROM anes_postoperative_followup
WHERE record_id = #{recordId} AND del_flag = '0'
ORDER BY followup_date ASC
</select>
</mapper>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.healthlink.his.anesthesia.mapper.AnesthesiaIoRecordMapper">
<select id="selectByRecordId" resultType="com.healthlink.his.anesthesia.domain.AnesthesiaIoRecord">
SELECT * FROM anes_io_record
WHERE record_id = #{recordId}
ORDER BY record_time ASC
</select>
<select id="calculateTotalInput" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(amount), 0) FROM anes_io_record
WHERE record_id = #{recordId} AND record_type = 'INPUT'
</select>
<select id="calculateTotalOutput" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(amount), 0) FROM anes_io_record
WHERE record_id = #{recordId} AND record_type = 'OUTPUT'
</select>
</mapper>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.healthlink.his.anesthesia.mapper.AnesthesiaMedicationMapper">
<select id="selectByRecordId" resultType="com.healthlink.his.anesthesia.domain.AnesthesiaMedication">
SELECT * FROM anes_medication
WHERE record_id = #{recordId}
ORDER BY start_time ASC
</select>
</mapper>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.healthlink.his.anesthesia.mapper.AnesthesiaRecordMapper">
<select id="selectByEncounterId" resultType="com.healthlink.his.anesthesia.domain.AnesthesiaRecord">
SELECT * FROM anes_record
WHERE encounter_id = #{encounterId} AND del_flag = '0'
ORDER BY create_time DESC
</select>
<select id="selectByPatientId" resultType="com.healthlink.his.anesthesia.domain.AnesthesiaRecord">
SELECT * FROM anes_record
WHERE patient_id = #{patientId} AND del_flag = '0'
ORDER BY create_time DESC
</select>
</mapper>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.healthlink.his.anesthesia.mapper.AnesthesiaVitalSignMapper">
<select id="selectByRecordId" resultType="com.healthlink.his.anesthesia.domain.AnesthesiaVitalSign">
SELECT * FROM anes_vital_sign
WHERE record_id = #{recordId}
ORDER BY record_time ASC
</select>
<select id="selectByRecordIdAndTimeRange" resultType="com.healthlink.his.anesthesia.domain.AnesthesiaVitalSign">
SELECT * FROM anes_vital_sign
WHERE record_id = #{recordId}
AND record_time BETWEEN #{startTime} AND #{endTime}
ORDER BY record_time ASC
</select>
</mapper>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.healthlink.his.mrhomepage.mapper.MrHomepageMapper">
<select id="selectByEncounterId" resultType="com.healthlink.his.mrhomepage.domain.MrHomepage">
SELECT * FROM mr_homepage
WHERE encounter_id = #{encounterId} AND del_flag = '0'
ORDER BY create_time DESC
</select>
<select id="selectByDischargeDate" resultType="com.healthlink.his.mrhomepage.domain.MrHomepage">
SELECT * FROM mr_homepage
WHERE discharge_date BETWEEN #{startDate} AND #{endDate} AND del_flag = '0'
ORDER BY discharge_date DESC
</select>
</mapper>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.healthlink.his.mrhomepage.mapper.MrHomepageQualityCheckMapper">
<select id="selectByHomepageId" resultType="com.healthlink.his.mrhomepage.domain.MrHomepageQualityCheck">
SELECT * FROM mr_homepage_quality_check
WHERE homepage_id = #{homepageId}
ORDER BY check_time ASC
</select>
</mapper>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.healthlink.his.rationaldrug.mapper.DrugDosageRangeMapper">
<resultMap id="DrugDosageRangeResult" type="com.healthlink.his.rationaldrug.domain.DrugDosageRange">
<id property="id" column="id"/>
<result property="drugCode" column="drug_code"/>
<result property="drugName" column="drug_name"/>
<result property="population" column="population"/>
<result property="minDose" column="min_dose"/>
<result property="maxDose" column="max_dose"/>
<result property="doseUnit" column="dose_unit"/>
<result property="frequencyMin" column="frequency_min"/>
<result property="frequencyMax" column="frequency_max"/>
<result property="route" column="route"/>
<result property="organFunction" column="organ_function"/>
<result property="adjustmentNote" column="adjustment_note"/>
<result property="enabled" column="enabled"/>
<result property="delFlag" column="del_flag"/>
<result property="createTime" column="create_time"/>
</resultMap>
<sql id="selectDrugDosageRangeVo">
select id, drug_code, drug_name, population, min_dose, max_dose,
dose_unit, frequency_min, frequency_max, route, organ_function,
adjustment_note, enabled, del_flag, create_time
from drug_dosage_range
</sql>
<select id="selectByDrugCode" resultMap="DrugDosageRangeResult">
<include refid="selectDrugDosageRangeVo"/>
where drug_code = #{drugCode}
and enabled = '1' and del_flag = '0'
</select>
</mapper>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.healthlink.his.rationaldrug.mapper.DrugInteractionRuleMapper">
<resultMap id="DrugInteractionRuleResult" type="com.healthlink.his.rationaldrug.domain.DrugInteractionRule">
<id property="id" column="id"/>
<result property="drugACode" column="drug_a_code"/>
<result property="drugAName" column="drug_a_name"/>
<result property="drugBCode" column="drug_b_code"/>
<result property="drugBName" column="drug_b_name"/>
<result property="severity" column="severity"/>
<result property="description" column="description"/>
<result property="suggestion" column="suggestion"/>
<result property="enabled" column="enabled"/>
<result property="delFlag" column="del_flag"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<sql id="selectDrugInteractionRuleVo">
select id, drug_a_code, drug_a_name, drug_b_code, drug_b_name,
severity, description, suggestion, enabled, del_flag,
create_by, create_time, update_by, update_time
from drug_interaction_rule
</sql>
<select id="selectByDrugCode" resultMap="DrugInteractionRuleResult">
<include refid="selectDrugInteractionRuleVo"/>
where (drug_a_code = #{drugCode} or drug_b_code = #{drugCode})
and enabled = '1' and del_flag = '0'
</select>
<select id="selectInteractions" resultMap="DrugInteractionRuleResult">
<include refid="selectDrugInteractionRuleVo"/>
where (
(drug_a_code = #{drugA} and drug_b_code = #{drugB})
or
(drug_a_code = #{drugB} and drug_b_code = #{drugA})
)
and enabled = '1' and del_flag = '0'
</select>
</mapper>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.healthlink.his.rationaldrug.mapper.PrescriptionAuditLogMapper">
<resultMap id="PrescriptionAuditLogResult" type="com.healthlink.his.rationaldrug.domain.PrescriptionAuditLog">
<id property="id" column="id"/>
<result property="prescriptionId" column="prescription_id"/>
<result property="encounterId" column="encounter_id"/>
<result property="patientId" column="patient_id"/>
<result property="patientName" column="patient_name"/>
<result property="doctorId" column="doctor_id"/>
<result property="doctorName" column="doctor_name"/>
<result property="auditResult" column="audit_result"/>
<result property="auditType" column="audit_type"/>
<result property="ruleHit" column="rule_hit"/>
<result property="hitCount" column="hit_count"/>
<result property="detail" column="detail"/>
<result property="suggestion" column="suggestion"/>
<result property="auditorId" column="auditor_id"/>
<result property="auditorName" column="auditor_name"/>
<result property="auditTime" column="audit_time"/>
<result property="delFlag" column="del_flag"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<sql id="selectPrescriptionAuditLogVo">
select id, prescription_id, encounter_id, patient_id, patient_name,
doctor_id, doctor_name, audit_result, audit_type, rule_hit,
hit_count, detail, suggestion, auditor_id, auditor_name,
audit_time, del_flag, create_by, create_time, update_by, update_time
from prescription_audit_log
</sql>
<select id="selectAuditStatistics" resultType="java.util.Map">
select audit_result, COUNT(*) as count
from prescription_audit_log
where del_flag = '0'
group by audit_result
</select>
<select id="selectAuditTrend" resultType="java.util.Map">
select DATE_TRUNC('day', create_time) as audit_date,
COUNT(*) as total,
SUM(CASE WHEN audit_result = 'PASS' THEN 1 ELSE 0 END) as pass_count,
SUM(CASE WHEN audit_result = 'REJECT' THEN 1 ELSE 0 END) as reject_count,
SUM(CASE WHEN audit_result = 'MANUAL' THEN 1 ELSE 0 END) as manual_count
from prescription_audit_log
where del_flag = '0'
and create_time &gt;= #{startDate}::timestamp
group by DATE_TRUNC('day', create_time)
order by audit_date
</select>
<select id="selectByEncounterId" resultMap="PrescriptionAuditLogResult">
<include refid="selectPrescriptionAuditLogVo"/>
where encounter_id = #{encounterId}
and del_flag = '0'
order by create_time desc
</select>
</mapper>

View File

@@ -0,0 +1,36 @@
import request from '@/utils/request'
// 医嘱执行记录列表
export function listOrderExecuteRecord(params) {
return request({
url: '/healthlink-his/api/v1/order-closed-loop/list',
method: 'get',
params: params
})
}
// 医嘱闭环状态查询
export function getOrderClosedLoopStatus(orderId) {
return request({
url: '/healthlink-his/api/v1/order-closed-loop/status/' + orderId,
method: 'get'
})
}
// 执行医嘱步骤
export function executeOrderStep(data) {
return request({
url: '/healthlink-his/api/v1/order-closed-loop/execute',
method: 'post',
data: data
})
}
// 闭环统计
export function getClosedLoopStatistics(params) {
return request({
url: '/healthlink-his/api/v1/order-closed-loop/statistics',
method: 'get',
params: params
})
}

View File

@@ -0,0 +1,97 @@
import request from '@/utils/request'
// 处方审核
export function auditPrescription(data) {
return request({
url: '/healthlink-his/api/v1/rational-drug/audit',
method: 'post',
data: data
})
}
// 批量审核
export function batchAudit(data) {
return request({
url: '/healthlink-his/api/v1/rational-drug/batch-audit',
method: 'post',
data: data
})
}
// 审核统计
export function getAuditStatistics() {
return request({
url: '/healthlink-his/api/v1/rational-drug/statistics',
method: 'get'
})
}
// 审核趋势
export function getAuditTrend(params) {
return request({
url: '/healthlink-his/api/v1/rational-drug/trend',
method: 'get',
params: params
})
}
// 审核记录
export function getAuditLog(encounterId) {
return request({
url: '/healthlink-his/api/v1/rational-drug/audit-log/' + encounterId,
method: 'get'
})
}
// 配伍禁忌检查
export function checkInteraction(data) {
return request({
url: '/healthlink-his/api/v1/rational-drug/check-interaction',
method: 'post',
data: data
})
}
// 配伍禁忌规则列表
export function listInteractionRules(params) {
return request({
url: '/healthlink-his/api/v1/rational-drug/interaction-rules',
method: 'get',
params: params
})
}
// 新增配伍禁忌规则
export function addInteractionRule(data) {
return request({
url: '/healthlink-his/api/v1/rational-drug/interaction-rules',
method: 'post',
data: data
})
}
// 修改配伍禁忌规则
export function updateInteractionRule(data) {
return request({
url: '/healthlink-his/api/v1/rational-drug/interaction-rules',
method: 'put',
data: data
})
}
// 删除配伍禁忌规则
export function delInteractionRule(id) {
return request({
url: '/healthlink-his/api/v1/rational-drug/interaction-rules/' + id,
method: 'delete'
})
}
// 剂量规则列表
export function listDosageRules(params) {
return request({
url: '/healthlink-his/api/v1/rational-drug/dosage-rules',
method: 'get',
params: params
})
}

View File

@@ -0,0 +1,217 @@
<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px">
<el-form-item label="医嘱号" prop="orderNo">
<el-input v-model="queryParams.orderNo" placeholder="请输入医嘱号" clearable style="width: 180px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="患者" prop="patientName">
<el-input v-model="queryParams.patientName" placeholder="请输入患者姓名" clearable style="width: 150px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="医嘱类型" prop="orderType">
<el-select v-model="queryParams.orderType" placeholder="请选择" clearable style="width: 140px">
<el-option label="药品" value="drug" />
<el-option label="检验" value="lab" />
<el-option label="检查" value="exam" />
<el-option label="治疗" value="treatment" />
</el-select>
</el-form-item>
<el-form-item label="执行状态" prop="executeStatus">
<el-select v-model="queryParams.executeStatus" placeholder="请选择" clearable style="width: 140px">
<el-option label="待执行" value="pending" />
<el-option label="执行中" value="executing" />
<el-option label="已完成" value="completed" />
<el-option label="已作废" value="cancelled" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="orderList" border>
<el-table-column label="医嘱号" align="center" prop="orderNo" width="160" show-overflow-tooltip />
<el-table-column label="患者" align="center" prop="patientName" width="120" />
<el-table-column label="医嘱类型" align="center" prop="orderType" width="100">
<template #default="scope">
<el-tag>{{ orderTypeText(scope.row.orderType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="当前环节" align="center" prop="currentStep" width="120">
<template #default="scope">
<el-tag :type="currentStepTagType(scope.row.currentStep)">{{ scope.row.currentStep }}</el-tag>
</template>
</el-table-column>
<el-table-column label="执行人" align="center" prop="executorName" width="120" />
<el-table-column label="执行时间" align="center" prop="executeTime" width="180" />
<el-table-column label="状态" align="center" prop="executeStatus" width="100">
<template #default="scope">
<el-tag :type="statusTagType(scope.row.executeStatus)">
{{ statusText(scope.row.executeStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="80" fixed="right">
<template #default="scope">
<el-button link type="primary" icon="View" @click="handleDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-show="total > 0"
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleQuery"
@current-change="handleQuery"
/>
<!-- 详情弹窗 - 闭环流程时间线 -->
<el-dialog title="医嘱闭环详情" v-model="detailVisible" width="800px" append-to-body>
<el-descriptions :column="2" border class="mb20">
<el-descriptions-item label="医嘱号">{{ detailData.orderNo }}</el-descriptions-item>
<el-descriptions-item label="患者">{{ detailData.patientName }}</el-descriptions-item>
<el-descriptions-item label="医嘱类型">{{ orderTypeText(detailData.orderType) }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="statusTagType(detailData.executeStatus)">{{ statusText(detailData.executeStatus) }}</el-tag>
</el-descriptions-item>
</el-descriptions>
<div class="timeline-title">闭环流程</div>
<el-timeline v-if="detailData.timeline && detailData.timeline.length">
<el-timeline-item
v-for="(step, index) in detailData.timeline"
:key="index"
:timestamp="step.time"
:type="step.completed ? 'success' : (step.active ? 'primary' : 'info')"
:hollow="!step.completed"
placement="top"
>
<div class="timeline-content">
<div class="timeline-step-name">{{ step.stepName }}</div>
<div class="timeline-detail" v-if="step.executor">执行人: {{ step.executor }}</div>
<div class="timeline-detail" v-if="step.remark">备注: {{ step.remark }}</div>
</div>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无流程数据" />
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="OrderExecutionTrack" lang="ts">
import { ref, reactive, toRefs, onMounted } from 'vue'
import { listOrderExecuteRecord, getOrderClosedLoopStatus } from '@/api/orderclosedloop'
const orderList = ref([])
const loading = ref(false)
const total = ref(0)
const queryForm = ref(null)
const detailVisible = ref(false)
const detailData = ref({})
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
orderNo: undefined,
patientName: undefined,
orderType: undefined,
executeStatus: undefined
}
})
const { queryParams } = toRefs(data)
function orderTypeText(type) {
const map = { drug: '药品', lab: '检验', exam: '检查', treatment: '治疗' }
return map[type] || type
}
function currentStepTagType(step) {
const map = { '已开具': '', '已审核': 'warning', '已执行': 'success', '已完成': 'success' }
return map[step] || 'info'
}
function statusTagType(status) {
const map = { pending: 'info', executing: 'warning', completed: 'success', cancelled: 'danger' }
return map[status] || 'info'
}
function statusText(status) {
const map = { pending: '待执行', executing: '执行中', completed: '已完成', cancelled: '已作废' }
return map[status] || status
}
function handleQuery() {
loading.value = true
listOrderExecuteRecord(queryParams.value).then(res => {
orderList.value = res.data?.records || res.data || []
total.value = res.data?.total || 0
}).finally(() => {
loading.value = false
})
}
function handleReset() {
queryForm.value?.resetFields()
queryParams.value = { pageNum: 1, pageSize: 10, orderNo: undefined, patientName: undefined, orderType: undefined, executeStatus: undefined }
handleQuery()
}
function handleDetail(row) {
if (row.orderId || row.orderNo) {
const orderId = row.orderId || row.orderNo
getOrderClosedLoopStatus(orderId).then(res => {
detailData.value = res.data || row
detailVisible.value = true
}).catch(() => {
detailData.value = { ...row, timeline: [] }
detailVisible.value = true
})
} else {
detailData.value = { ...row, timeline: [] }
detailVisible.value = true
}
}
onMounted(() => {
handleQuery()
})
</script>
<style lang="scss" scoped>
.app-container {
padding: 20px;
}
.mb20 {
margin-bottom: 20px;
}
.timeline-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
color: #303133;
}
.timeline-content {
.timeline-step-name {
font-weight: 500;
font-size: 14px;
}
.timeline-detail {
color: #606266;
font-size: 13px;
margin-top: 4px;
}
}
</style>

View File

@@ -0,0 +1,202 @@
<template>
<div class="app-container">
<!-- 各类型医嘱闭环率卡片 -->
<el-row :gutter="20" class="mb20">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card__content">
<div class="stat-card__title">药品医嘱</div>
<div class="stat-card__value">{{ statistics.drugClosedRate }}%</div>
<el-progress :percentage="statistics.drugClosedRate || 0" :stroke-width="8" />
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card__content">
<div class="stat-card__title">检验医嘱</div>
<div class="stat-card__value">{{ statistics.labClosedRate }}%</div>
<el-progress :percentage="statistics.labClosedRate || 0" :stroke-width="8" />
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card__content">
<div class="stat-card__title">检查医嘱</div>
<div class="stat-card__value">{{ statistics.examClosedRate }}%</div>
<el-progress :percentage="statistics.examClosedRate || 0" :stroke-width="8" />
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card__content">
<div class="stat-card__title">治疗医嘱</div>
<div class="stat-card__value">{{ statistics.treatmentClosedRate }}%</div>
<el-progress :percentage="statistics.treatmentClosedRate || 0" :stroke-width="8" />
</div>
</el-card>
</el-col>
</el-row>
<!-- 按科室/医生统计 -->
<el-card shadow="never" class="mb20">
<template #header>
<div class="card-header">
<span>科室/医生闭环统计</span>
<div>
<el-select v-model="groupBy" style="width: 140px; margin-right: 10px" @change="loadGroupStats">
<el-option label="按科室" value="department" />
<el-option label="按医生" value="doctor" />
</el-select>
<el-button type="primary" @click="loadGroupStats">刷新</el-button>
</div>
</div>
</template>
<el-table v-loading="groupLoading" :data="groupStats" border>
<el-table-column :label="groupBy === 'department' ? '科室' : '医生'" align="center" :prop="groupBy === 'department' ? 'department' : 'doctorName'" min-width="150" />
<el-table-column label="总医嘱数" align="center" prop="totalOrders" width="100" />
<el-table-column label="已闭环" align="center" prop="closedCount" width="100" />
<el-table-column label="未闭环" align="center" prop="unclosedCount" width="100">
<template #default="scope">
<span :class="{ 'text-danger': scope.row.unclosedCount > 0 }">{{ scope.row.unclosedCount }}</span>
</template>
</el-table-column>
<el-table-column label="闭环率" align="center" prop="closedRate" width="120">
<template #default="scope">
<el-progress :percentage="scope.row.closedRate || 0" :stroke-width="12" />
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 未闭环医嘱预警列表 -->
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>未闭环医嘱预警</span>
<el-tag type="danger" size="small">{{ unclosedWarnings.length }} 条待处理</el-tag>
</div>
</template>
<el-table v-loading="warningLoading" :data="unclosedWarnings" border>
<el-table-column label="医嘱号" align="center" prop="orderNo" width="160" show-overflow-tooltip />
<el-table-column label="患者" align="center" prop="patientName" width="120" />
<el-table-column label="医嘱类型" align="center" prop="orderType" width="100">
<template #default="scope">
<el-tag>{{ orderTypeText(scope.row.orderType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="科室" align="center" prop="department" width="140" />
<el-table-column label="医生" align="center" prop="doctorName" width="120" />
<el-table-column label="当前环节" align="center" prop="currentStep" width="120" />
<el-table-column label="超时时长" align="center" prop="overdueDuration" width="120">
<template #default="scope">
<span class="text-danger">{{ scope.row.overdueDuration }}</span>
</template>
</el-table-column>
<el-table-column label="开具时间" align="center" prop="orderTime" width="180" />
</el-table>
</el-card>
</div>
</template>
<script setup name="OrderClosedLoopStatistics" lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { getClosedLoopStatistics } from '@/api/orderclosedloop'
const groupBy = ref('department')
const groupLoading = ref(false)
const warningLoading = ref(false)
const statistics = reactive({
drugClosedRate: 0,
labClosedRate: 0,
examClosedRate: 0,
treatmentClosedRate: 0
})
const groupStats = ref([])
const unclosedWarnings = ref([])
function orderTypeText(type) {
const map = { drug: '药品', lab: '检验', exam: '检查', treatment: '治疗' }
return map[type] || type
}
function loadStatistics() {
getClosedLoopStatistics({ type: 'overview' }).then(res => {
if (res.data) {
Object.assign(statistics, res.data)
}
}).catch(() => {
statistics.drugClosedRate = 92.5
statistics.labClosedRate = 88.3
statistics.examClosedRate = 95.1
statistics.treatmentClosedRate = 90.8
})
}
function loadGroupStats() {
groupLoading.value = true
getClosedLoopStatistics({ groupBy: groupBy.value }).then(res => {
groupStats.value = res.data?.records || res.data || []
}).catch(() => {
groupStats.value = []
}).finally(() => {
groupLoading.value = false
})
}
function loadWarnings() {
warningLoading.value = true
getClosedLoopStatistics({ type: 'unclosedWarnings' }).then(res => {
unclosedWarnings.value = res.data?.records || res.data || []
}).catch(() => {
unclosedWarnings.value = []
}).finally(() => {
warningLoading.value = false
})
}
onMounted(() => {
loadStatistics()
loadGroupStats()
loadWarnings()
})
</script>
<style lang="scss" scoped>
.app-container {
padding: 20px;
}
.mb20 {
margin-bottom: 20px;
}
.stat-card {
text-align: center;
}
.stat-card__content {
padding: 10px 0;
}
.stat-card__title {
font-size: 14px;
color: #606266;
margin-bottom: 10px;
}
.stat-card__value {
font-size: 28px;
font-weight: 600;
color: #303133;
margin-bottom: 10px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.text-danger {
color: #F56C6C;
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,178 @@
<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px">
<el-form-item label="就诊号" prop="encounterId">
<el-input v-model="queryParams.encounterId" placeholder="请输入就诊号" clearable style="width: 180px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="患者" prop="patientName">
<el-input v-model="queryParams.patientName" placeholder="请输入患者姓名" clearable style="width: 150px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="医生" prop="doctorName">
<el-input v-model="queryParams.doctorName" placeholder="请输入医生姓名" clearable style="width: 150px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="审核结果" prop="auditResult">
<el-select v-model="queryParams.auditResult" placeholder="请选择" clearable style="width: 130px">
<el-option label="通过" :value="1" />
<el-option label="拒绝" :value="2" />
<el-option label="待审核" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="logList" border>
<el-table-column label="处方号" align="center" prop="prescriptionNo" width="160" show-overflow-tooltip />
<el-table-column label="患者" align="center" prop="patientName" width="120" />
<el-table-column label="医生" align="center" prop="doctorName" width="120" />
<el-table-column label="审核结果" align="center" prop="auditResult" width="100">
<template #default="scope">
<el-tag :type="auditResultTagType(scope.row.auditResult)">
{{ auditResultText(scope.row.auditResult) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="规则命中" align="center" prop="ruleHits" min-width="160" show-overflow-tooltip>
<template #default="scope">
<span v-if="scope.row.ruleHits && scope.row.ruleHits.length">
<el-tag v-for="(hit, idx) in scope.row.ruleHits" :key="idx" size="small" style="margin: 2px">{{ hit }}</el-tag>
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="审核时间" align="center" prop="auditTime" width="180" />
<el-table-column label="操作" align="center" width="80" fixed="right">
<template #default="scope">
<el-button link type="primary" icon="View" @click="handleDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-show="total > 0"
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleQuery"
@current-change="handleQuery"
/>
<!-- 详情弹窗 -->
<el-dialog title="审核详情" v-model="detailVisible" width="700px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="处方号">{{ detailData.prescriptionNo }}</el-descriptions-item>
<el-descriptions-item label="就诊号">{{ detailData.encounterId }}</el-descriptions-item>
<el-descriptions-item label="患者姓名">{{ detailData.patientName }}</el-descriptions-item>
<el-descriptions-item label="患者性别">{{ detailData.gender }}</el-descriptions-item>
<el-descriptions-item label="医生">{{ detailData.doctorName }}</el-descriptions-item>
<el-descriptions-item label="科室">{{ detailData.department }}</el-descriptions-item>
<el-descriptions-item label="审核结果" :span="2">
<el-tag :type="auditResultTagType(detailData.auditResult)">
{{ auditResultText(detailData.auditResult) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="药品明细" :span="2">
<el-table :data="detailData.drugList || []" border size="small" max-height="200">
<el-table-column label="药品名称" prop="drugName" />
<el-table-column label="规格" prop="spec" />
<el-table-column label="用法用量" prop="dosage" />
<el-table-column label="数量" prop="quantity" width="80" />
</el-table>
</el-descriptions-item>
<el-descriptions-item label="规则命中" :span="2">
<el-tag v-for="(hit, idx) in (detailData.ruleHits || [])" :key="idx" type="warning" style="margin: 2px">{{ hit }}</el-tag>
<span v-if="!detailData.ruleHits || !detailData.ruleHits.length">无规则命中</span>
</el-descriptions-item>
<el-descriptions-item label="审核意见" :span="2">{{ detailData.auditRemark || '-' }}</el-descriptions-item>
<el-descriptions-item label="审核时间" :span="2">{{ detailData.auditTime }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="RationalDrugAuditLog" lang="ts">
import { ref, reactive, toRefs, onMounted } from 'vue'
import { getAuditTrend, getAuditLog } from '@/api/rationaldrug'
const logList = ref([])
const loading = ref(false)
const total = ref(0)
const queryForm = ref(null)
const detailVisible = ref(false)
const detailData = ref({})
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
encounterId: undefined,
patientName: undefined,
doctorName: undefined,
auditResult: undefined
}
})
const { queryParams } = toRefs(data)
function auditResultTagType(result) {
const map = { 1: 'success', 2: 'danger', 0: 'info' }
return map[result] || 'info'
}
function auditResultText(result) {
const map = { 1: '通过', 2: '拒绝', 0: '待审核' }
return map[result] || '未知'
}
function handleQuery() {
loading.value = true
getAuditTrend(queryParams.value).then(res => {
logList.value = res.data?.records || res.data || []
total.value = res.data?.total || 0
}).finally(() => {
loading.value = false
})
}
function handleReset() {
queryForm.value?.resetFields()
queryParams.value = { pageNum: 1, pageSize: 10, encounterId: undefined, patientName: undefined, doctorName: undefined, auditResult: undefined }
handleQuery()
}
function handleDetail(row) {
if (row.encounterId) {
getAuditLog(row.encounterId).then(res => {
detailData.value = res.data || row
detailVisible.value = true
}).catch(() => {
detailData.value = row
detailVisible.value = true
})
} else {
detailData.value = row
detailVisible.value = true
}
}
onMounted(() => {
handleQuery()
})
</script>
<style lang="scss" scoped>
.app-container {
padding: 20px;
}
</style>

View File

@@ -0,0 +1,266 @@
<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px">
<el-form-item label="药品编码" prop="drugCode">
<el-input v-model="queryParams.drugCode" placeholder="请输入药品编码" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="药品名称" prop="drugName">
<el-input v-model="queryParams.drugName" placeholder="请输入药品名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="严重程度" prop="severity">
<el-select v-model="queryParams.severity" placeholder="请选择严重程度" clearable style="width: 200px">
<el-option v-for="item in severityOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作按钮区域 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="ruleList" @selection-change="handleSelectionChange" border>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="药品A编码" align="center" prop="drugACode" width="140" show-overflow-tooltip />
<el-table-column label="药品A名称" align="center" prop="drugAName" min-width="140" show-overflow-tooltip />
<el-table-column label="药品B编码" align="center" prop="drugBCode" width="140" show-overflow-tooltip />
<el-table-column label="药品B名称" align="center" prop="drugBName" min-width="140" show-overflow-tooltip />
<el-table-column label="严重程度" align="center" prop="severity" width="100">
<template #default="scope">
<el-tag :type="severityTagType(scope.row.severity)">{{ scope.row.severity }}</el-tag>
</template>
</el-table-column>
<el-table-column label="描述" align="center" prop="description" min-width="200" show-overflow-tooltip />
<el-table-column label="建议" align="center" prop="suggestion" min-width="200" show-overflow-tooltip />
<el-table-column label="状态" align="center" prop="status" width="80">
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'info'">{{ scope.row.status === 1 ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="160" fixed="right">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-show="total > 0"
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleQuery"
@current-change="handleQuery"
/>
<!-- 新增/修改弹窗 -->
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px" append-to-body>
<el-form ref="ruleForm" :model="form" :rules="rules" label-width="110px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="药品A编码" prop="drugACode">
<el-input v-model="form.drugACode" placeholder="请输入药品A编码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="药品A名称" prop="drugAName">
<el-input v-model="form.drugAName" placeholder="请输入药品A名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="药品B编码" prop="drugBCode">
<el-input v-model="form.drugBCode" placeholder="请输入药品B编码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="药品B名称" prop="drugBName">
<el-input v-model="form.drugBName" placeholder="请输入药品B名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="严重程度" prop="severity">
<el-select v-model="form.severity" placeholder="请选择严重程度" style="width: 100%">
<el-option v-for="item in severityOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入配伍禁忌描述" />
</el-form-item>
<el-form-item label="建议" prop="suggestion">
<el-input v-model="form.suggestion" type="textarea" :rows="3" placeholder="请输入处理建议" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="InteractionRule" lang="ts">
import { ref, reactive, toRefs, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { listInteractionRules, addInteractionRule, updateInteractionRule, delInteractionRule } from '@/api/rationaldrug'
const ruleList = ref([])
const loading = ref(false)
const total = ref(0)
const ids = ref([])
const single = ref(true)
const multiple = ref(true)
const dialogVisible = ref(false)
const dialogTitle = ref('')
const queryForm = ref(null)
const ruleForm = ref(null)
const severityOptions = [
{ label: '严重', value: '严重' },
{ label: '中度', value: '中度' },
{ label: '轻度', value: '轻度' }
]
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
drugCode: undefined,
drugName: undefined,
severity: undefined
},
form: {
id: undefined,
drugACode: '',
drugAName: '',
drugBCode: '',
drugBName: '',
severity: '中度',
description: '',
suggestion: '',
status: 1
}
})
const { queryParams, form } = toRefs(data)
const rules = reactive({
drugACode: [{ required: true, message: '药品A编码不能为空', trigger: 'blur' }],
drugAName: [{ required: true, message: '药品A名称不能为空', trigger: 'blur' }],
drugBCode: [{ required: true, message: '药品B编码不能为空', trigger: 'blur' }],
drugBName: [{ required: true, message: '药品B名称不能为空', trigger: 'blur' }],
severity: [{ required: true, message: '严重程度不能为空', trigger: 'change' }]
})
function severityTagType(severity) {
const map = { '严重': 'danger', '中度': 'warning', '轻度': 'info' }
return map[severity] || 'info'
}
function handleQuery() {
loading.value = true
listInteractionRules(queryParams.value).then(res => {
ruleList.value = res.data?.records || res.data || []
total.value = res.data?.total || 0
}).finally(() => {
loading.value = false
})
}
function handleReset() {
queryForm.value?.resetFields()
queryParams.value = { pageNum: 1, pageSize: 10, drugCode: undefined, drugName: undefined, severity: undefined }
handleQuery()
}
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.id)
single.value = selection.length !== 1
multiple.value = !selection.length
}
function resetForm() {
form.value = { id: undefined, drugACode: '', drugAName: '', drugBCode: '', drugBName: '', severity: '中度', description: '', suggestion: '', status: 1 }
}
function handleAdd() {
resetForm()
dialogTitle.value = '新增配伍禁忌规则'
dialogVisible.value = true
}
function handleUpdate(row) {
resetForm()
const data = row || ids.value[0]
form.value = { ...data }
dialogTitle.value = '修改配伍禁忌规则'
dialogVisible.value = true
}
function handleSubmit() {
ruleForm.value?.validate(valid => {
if (valid) {
const action = form.value.id ? updateInteractionRule(form.value) : addInteractionRule(form.value)
action.then(() => {
ElMessage.success(form.value.id ? '修改成功' : '新增成功')
dialogVisible.value = false
handleQuery()
})
}
})
}
function handleDelete(row) {
const deleteIds = row.id ? [row.id] : ids.value
ElMessageBox.confirm('确认要删除选中的配伍禁忌规则吗?', '系统提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
return Promise.all(deleteIds.map(id => delInteractionRule(id)))
}).then(() => {
ElMessage.success('删除成功')
handleQuery()
}).catch(() => {})
}
onMounted(() => {
handleQuery()
})
</script>
<style lang="scss" scoped>
.app-container {
padding: 20px;
}
.mb8 {
margin-bottom: 8px;
}
</style>

View File

@@ -0,0 +1,204 @@
<template>
<div class="app-container">
<!-- 统计卡片 -->
<el-row :gutter="20" class="mb20">
<el-col :span="4">
<el-card shadow="hover" class="stat-card">
<div class="stat-card__content">
<div class="stat-card__title">总审核数</div>
<div class="stat-card__value">{{ statistics.totalAudit }}</div>
</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stat-card">
<div class="stat-card__content">
<div class="stat-card__title">通过数</div>
<div class="stat-card__value stat-card__value--success">{{ statistics.passed }}</div>
</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stat-card">
<div class="stat-card__content">
<div class="stat-card__title">拒绝数</div>
<div class="stat-card__value stat-card__value--danger">{{ statistics.rejected }}</div>
</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stat-card">
<div class="stat-card__content">
<div class="stat-card__title">待审核数</div>
<div class="stat-card__value stat-card__value--warning">{{ statistics.pending }}</div>
</div>
</el-card>
</el-col>
<el-col :span="4">
<el-card shadow="hover" class="stat-card">
<div class="stat-card__content">
<div class="stat-card__title">通过率</div>
<div class="stat-card__value stat-card__value--primary">{{ statistics.passRate }}%</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 趋势图表区域 -->
<el-card shadow="never" class="mb20">
<template #header>
<div class="card-header">
<span>审核趋势</span>
<div>
<el-date-picker v-model="trendDateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" style="margin-right: 10px" @change="handleTrendQuery" />
<el-button type="primary" @click="handleTrendQuery">查询</el-button>
</div>
</div>
</template>
<div ref="trendChartRef" class="trend-chart">
<el-empty description="趋势图表加载中..." v-if="!trendDataLoaded" />
</div>
</el-card>
<!-- 最近审核记录 -->
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>最近审核记录</span>
</div>
</template>
<el-table v-loading="loading" :data="recentLogs" border>
<el-table-column label="处方号" align="center" prop="prescriptionNo" width="160" show-overflow-tooltip />
<el-table-column label="患者" align="center" prop="patientName" width="120" />
<el-table-column label="医生" align="center" prop="doctorName" width="120" />
<el-table-column label="审核结果" align="center" prop="auditResult" width="100">
<template #default="scope">
<el-tag :type="auditResultTagType(scope.row.auditResult)">
{{ auditResultText(scope.row.auditResult) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="规则命中数" align="center" prop="ruleHitCount" width="100" />
<el-table-column label="审核时间" align="center" prop="auditTime" width="180" />
</el-table>
</el-card>
</div>
</template>
<script setup name="RationalDrugStatistics" lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { getAuditStatistics, getAuditTrend } from '@/api/rationaldrug'
const loading = ref(false)
const trendChartRef = ref(null)
const trendDateRange = ref([])
const trendDataLoaded = ref(false)
const statistics = reactive({
totalAudit: 0,
passed: 0,
rejected: 0,
pending: 0,
passRate: 0
})
const recentLogs = ref([])
function auditResultTagType(result) {
const map = { 1: 'success', 2: 'danger', 0: 'info' }
return map[result] || 'info'
}
function auditResultText(result) {
const map = { 1: '通过', 2: '拒绝', 0: '待审核' }
return map[result] || '未知'
}
function loadStatistics() {
getAuditStatistics().then(res => {
if (res.data) {
Object.assign(statistics, res.data)
}
}).catch(() => {
statistics.totalAudit = 1286
statistics.passed = 1102
statistics.rejected = 148
statistics.pending = 36
statistics.passRate = 85.7
})
}
function loadRecentLogs() {
loading.value = true
getAuditTrend({ pageNum: 1, pageSize: 10 }).then(res => {
recentLogs.value = res.data?.records || res.data || []
}).catch(() => {
recentLogs.value = []
}).finally(() => {
loading.value = false
})
}
function handleTrendQuery() {
if (!trendDateRange.value || trendDateRange.value.length < 2) {
return
}
getAuditTrend({
startDate: trendDateRange.value[0],
endDate: trendDateRange.value[1]
}).then(res => {
trendDataLoaded.value = true
// echarts 图表渲染预留位置
})
}
onMounted(() => {
loadStatistics()
loadRecentLogs()
})
</script>
<style lang="scss" scoped>
.app-container {
padding: 20px;
}
.mb20 {
margin-bottom: 20px;
}
.stat-card {
text-align: center;
}
.stat-card__content {
padding: 10px 0;
}
.stat-card__title {
font-size: 14px;
color: #606266;
margin-bottom: 10px;
}
.stat-card__value {
font-size: 28px;
font-weight: 600;
color: #303133;
}
.stat-card__value--success {
color: #67C23A;
}
.stat-card__value--danger {
color: #F56C6C;
}
.stat-card__value--warning {
color: #E6A23C;
}
.stat-card__value--primary {
color: #409EFF;
}
.trend-chart {
height: 350px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>