feat(P0): 电子病历增强+病案管理+护理评估增强+FHIR/CDA标准接口

V18 Flyway迁移 — 10张新表:
- emr_search_index: 病历全文检索索引
- mr_borrowing: 病案借阅管理(申请/审批/归还)
- mr_sealing: 病案封存管理(主动/纠纷/司法封存+解封)
- mr_tracking: 病案示踪(在架/借出/归档/遗失)
- mr_death_discussion: 死亡病例讨论(7天期限+超时预警)
- nursing_assessment_reminder: 护理评估提醒(跌倒/压疮/营养/疼痛/管道)
- nursing_care_plan: 护理计划(诊断/目标/措施/评价)
- esb_fhir_resource: FHIR R4资源存储(Patient/Encounter/Observation等)
- esb_cda_document: CDA临床文档架构(admission/discharge/lab等)
- esb_code_mapping: 标准编码映射

后端Controller:
- MrManagementController: 借阅/封存/示踪/死亡讨论完整CRUD
- NursingEnhancedController: 评估提醒/护理计划/质量指标
- FhirCdaController: FHIR资源CRUD+CDA文档+编码映射+翻译
- EmrSearchController: 多维度病历检索(关键词/患者/类型/医生/科室)

前端页面:
- mrmanagement: Tab页(借阅/封存/示踪/死亡讨论)
- nursingenhanced: Tab页(评估提醒/护理计划/质量指标)
- fhircda: Tab页(FHIR资源/CDA文档/编码映射)
- emrsearch: 多维度病历检索页

所有模块编译通过 (mvn clean compile -DskipTests)
This commit is contained in:
2026-06-06 16:48:35 +08:00
parent f68fe39897
commit be448fe092
53 changed files with 1909 additions and 13 deletions

View File

@@ -0,0 +1,101 @@
package com.healthlink.his.web.emr.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.healthlink.his.emr.domain.EmrSearchIndex;
import com.healthlink.his.emr.service.IEmrSearchIndexService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* 病历检索Controller — 多维度检索
*/
@RestController
@RequestMapping("/emr-search")
@Slf4j
@AllArgsConstructor
public class EmrSearchController {
private final IEmrSearchIndexService searchIndexService;
/**
* 多维度病历检索
*/
@GetMapping("/search")
public R<?> search(
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "patientName", required = false) String patientName,
@RequestParam(value = "emrType", required = false) String emrType,
@RequestParam(value = "doctorName", required = false) String doctorName,
@RequestParam(value = "departmentName", required = false) String departmentName,
@RequestParam(value = "startDate", required = false) String startDate,
@RequestParam(value = "endDate", required = false) String endDate,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<EmrSearchIndex> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(keyword)) {
wrapper.and(w -> w.like(EmrSearchIndex::getDiagnosisText, keyword)
.or().like(EmrSearchIndex::getEmrTitle, keyword)
.or().like(EmrSearchIndex::getPatientName, keyword));
}
wrapper.like(StringUtils.hasText(patientName), EmrSearchIndex::getPatientName, patientName)
.eq(StringUtils.hasText(emrType), EmrSearchIndex::getEmrType, emrType)
.like(StringUtils.hasText(doctorName), EmrSearchIndex::getDoctorName, doctorName)
.like(StringUtils.hasText(departmentName), EmrSearchIndex::getDepartmentName, departmentName)
.ge(StringUtils.hasText(startDate), EmrSearchIndex::getCreateTime, startDate)
.le(StringUtils.hasText(endDate), EmrSearchIndex::getCreateTime, endDate)
.orderByDesc(EmrSearchIndex::getCreateTime);
return R.ok(searchIndexService.page(new Page<>(pageNo, pageSize), wrapper));
}
/**
* 索引病历(新增/修改时调用)
*/
@PostMapping("/index")
@Transactional(rollbackFor = Exception.class)
public R<?> indexEmr(@RequestBody EmrSearchIndex index) {
// 检查是否已存在
LambdaQueryWrapper<EmrSearchIndex> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(EmrSearchIndex::getEmrId, index.getEmrId());
EmrSearchIndex existing = searchIndexService.getOne(wrapper);
if (existing != null) {
existing.setPatientName(index.getPatientName());
existing.setEmrType(index.getEmrType());
existing.setEmrTitle(index.getEmrTitle());
existing.setDiagnosisText(index.getDiagnosisText());
existing.setDoctorName(index.getDoctorName());
existing.setDepartmentName(index.getDepartmentName());
existing.setUpdateTime(new Date());
searchIndexService.updateById(existing);
return R.ok(existing);
} else {
index.setCreateTime(new Date());
searchIndexService.save(index);
return R.ok(index);
}
}
/**
* 检索统计
*/
@GetMapping("/stats")
public R<?> getStats() {
Map<String, Object> stats = new HashMap<>();
stats.put("totalIndexed", searchIndexService.count());
// 按类型统计
LambdaQueryWrapper<EmrSearchIndex> wrapper = new LambdaQueryWrapper<>();
String[] types = {"admission", "daily", "discharge", "surgery", "consultation"};
for (String type : types) {
wrapper.clear();
wrapper.eq(EmrSearchIndex::getEmrType, type);
stats.put("type_" + type, searchIndexService.count(wrapper));
}
return R.ok(stats);
}
}

View File

@@ -0,0 +1,163 @@
package com.healthlink.his.web.esbmanage.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.healthlink.his.esb.domain.CdaDocument;
import com.healthlink.his.esb.domain.CodeMapping;
import com.healthlink.his.esb.domain.FhirResource;
import com.healthlink.his.esb.service.ICdaDocumentService;
import com.healthlink.his.esb.service.ICodeMappingService;
import com.healthlink.his.esb.service.IFhirResourceService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* FHIR/CDA标准接口Controller — 互联互通测评核心
*/
@RestController
@RequestMapping("/fhir-cda")
@Slf4j
@AllArgsConstructor
public class FhirCdaController {
private final IFhirResourceService fhirService;
private final ICdaDocumentService cdaService;
private final ICodeMappingService mappingService;
// ==================== FHIR资源 ====================
@GetMapping("/fhir/page")
public R<?> getFhirPage(
@RequestParam(value = "resourceType", required = false) String resourceType,
@RequestParam(value = "patientId", required = false) Long patientId,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<FhirResource> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasText(resourceType), FhirResource::getResourceType, resourceType)
.eq(patientId != null, FhirResource::getPatientId, patientId)
.orderByDesc(FhirResource::getCreateTime);
return R.ok(fhirService.page(new Page<>(pageNo, pageSize), wrapper));
}
@PostMapping("/fhir/create")
@Transactional(rollbackFor = Exception.class)
public R<?> createFhirResource(@RequestBody FhirResource resource) {
resource.setStatus("ACTIVE");
resource.setVersionId(1);
resource.setCreateTime(new Date());
fhirService.save(resource);
return R.ok(resource);
}
@PutMapping("/fhir/update")
@Transactional(rollbackFor = Exception.class)
public R<?> updateFhirResource(@RequestBody FhirResource resource) {
FhirResource existing = fhirService.getById(resource.getId());
if (existing == null) return R.fail("资源不存在");
resource.setVersionId(existing.getVersionId() + 1);
resource.setUpdateTime(new Date());
fhirService.updateById(resource);
return R.ok();
}
@DeleteMapping("/fhir/delete")
@Transactional(rollbackFor = Exception.class)
public R<?> deleteFhirResource(@RequestParam Long id) {
fhirService.removeById(id);
return R.ok();
}
@GetMapping("/fhir/type-stats")
public R<?> getFhirTypeStats() {
Map<String, Object> stats = new HashMap<>();
String[] types = {"Patient", "Encounter", "Observation", "Condition", "MedicationRequest"};
for (String type : types) {
LambdaQueryWrapper<FhirResource> w = new LambdaQueryWrapper<>();
w.eq(FhirResource::getResourceType, type);
stats.put(type, fhirService.count(w));
}
stats.put("total", fhirService.count());
return R.ok(stats);
}
// ==================== CDA文档 ====================
@GetMapping("/cda/page")
public R<?> getCdaPage(
@RequestParam(value = "documentType", required = false) String documentType,
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<CdaDocument> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasText(documentType), CdaDocument::getDocumentType, documentType)
.eq(StringUtils.hasText(status), CdaDocument::getStatus, status)
.orderByDesc(CdaDocument::getCreateTime);
return R.ok(cdaService.page(new Page<>(pageNo, pageSize), wrapper));
}
@PostMapping("/cda/create")
@Transactional(rollbackFor = Exception.class)
public R<?> createCdaDocument(@RequestBody CdaDocument doc) {
doc.setStatus("DRAFT");
doc.setVersionId(1);
doc.setCreateTime(new Date());
cdaService.save(doc);
return R.ok(doc);
}
@PostMapping("/cda/publish")
@Transactional(rollbackFor = Exception.class)
public R<?> publishCdaDocument(@RequestParam Long id) {
CdaDocument doc = cdaService.getById(id);
if (doc == null) return R.fail("文档不存在");
doc.setStatus("PUBLISHED");
doc.setUpdateTime(new Date());
cdaService.updateById(doc);
return R.ok();
}
// ==================== 编码映射 ====================
@GetMapping("/mapping/page")
public R<?> getMappingPage(
@RequestParam(value = "mappingType", required = false) String mappingType,
@RequestParam(value = "sourceSystem", required = false) String sourceSystem,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<CodeMapping> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasText(mappingType), CodeMapping::getMappingType, mappingType)
.eq(StringUtils.hasText(sourceSystem), CodeMapping::getSourceSystem, sourceSystem)
.orderByDesc(CodeMapping::getCreateTime);
return R.ok(mappingService.page(new Page<>(pageNo, pageSize), wrapper));
}
@PostMapping("/mapping/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addMapping(@RequestBody CodeMapping mapping) {
mapping.setCreateTime(new Date());
mappingService.save(mapping);
return R.ok(mapping);
}
@PostMapping("/mapping/translate")
@Transactional(rollbackFor = Exception.class)
public R<?> translateCode(@RequestBody Map<String, String> params) {
LambdaQueryWrapper<CodeMapping> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CodeMapping::getSourceSystem, params.get("sourceSystem"))
.eq(CodeMapping::getSourceCode, params.get("sourceCode"))
.eq(CodeMapping::getMappingType, params.get("mappingType"));
CodeMapping mapping = mappingService.getOne(wrapper);
if (mapping == null) return R.fail("未找到映射关系");
Map<String, String> result = new HashMap<>();
result.put("targetCode", mapping.getTargetCode());
result.put("targetSystem", mapping.getTargetSystem());
result.put("description", mapping.getDescription());
return R.ok(result);
}
}

View File

@@ -0,0 +1,218 @@
package com.healthlink.his.web.mrhomepage.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.healthlink.his.mrhomepage.domain.*;
import com.healthlink.his.mrhomepage.service.*;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* 病案管理Controller — 借阅/封存/示踪/死亡讨论
*/
@RestController
@RequestMapping("/mr-management")
@Slf4j
@AllArgsConstructor
public class MrManagementController {
private final IMrBorrowingService borrowingService;
private final IMrSealingService sealingService;
private final IMrTrackingService trackingService;
private final IMrDeathDiscussionService deathDiscussionService;
// ==================== 病案借阅 ====================
@GetMapping("/borrowing/page")
public R<?> getBorrowingPage(
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "borrowerName", required = false) String borrowerName,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<MrBorrowing> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(status != null, MrBorrowing::getStatus, status)
.like(StringUtils.hasText(borrowerName), MrBorrowing::getBorrowerName, borrowerName)
.orderByDesc(MrBorrowing::getCreateTime);
return R.ok(borrowingService.page(new Page<>(pageNo, pageSize), wrapper));
}
@PostMapping("/borrowing/apply")
@Transactional(rollbackFor = Exception.class)
public R<?> applyBorrowing(@RequestBody MrBorrowing borrowing) {
borrowing.setStatus(0);
borrowing.setBorrowDate(new Date());
borrowing.setCreateTime(new Date());
borrowingService.save(borrowing);
return R.ok(borrowing);
}
@PostMapping("/borrowing/approve")
@Transactional(rollbackFor = Exception.class)
public R<?> approveBorrowing(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
String approver = (String) params.get("approverName");
Boolean approved = Boolean.valueOf(params.get("approved").toString());
MrBorrowing borrowing = borrowingService.getById(id);
if (borrowing == null) return R.fail("借阅记录不存在");
borrowing.setApproverName(approver);
borrowing.setApproveTime(new Date());
borrowing.setStatus(approved ? 1 : 5);
borrowing.setUpdateTime(new Date());
borrowingService.updateById(borrowing);
return R.ok();
}
@PostMapping("/borrowing/return")
@Transactional(rollbackFor = Exception.class)
public R<?> returnBorrowing(@RequestParam Long id) {
MrBorrowing borrowing = borrowingService.getById(id);
if (borrowing == null) return R.fail("借阅记录不存在");
borrowing.setActualReturnDate(new Date());
borrowing.setStatus(3);
borrowing.setUpdateTime(new Date());
borrowingService.updateById(borrowing);
// 更新示踪状态
LambdaQueryWrapper<MrTracking> tw = new LambdaQueryWrapper<>();
tw.eq(MrTracking::getMedicalRecordId, borrowing.getMedicalRecordId())
.eq(MrTracking::getStatus, "BORROWED");
MrTracking track = trackingService.getOne(tw);
if (track != null) {
track.setStatus("IN_SHELF");
track.setLocation("病案室");
track.setLocationType("STORAGE");
track.setMovedBy("系统自动归还");
track.setMoveTime(new Date());
trackingService.updateById(track);
}
return R.ok();
}
// ==================== 病案封存 ====================
@GetMapping("/sealing/page")
public R<?> getSealingPage(
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<MrSealing> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(status != null, MrSealing::getStatus, status)
.orderByDesc(MrSealing::getCreateTime);
return R.ok(sealingService.page(new Page<>(pageNo, pageSize), wrapper));
}
@PostMapping("/sealing/seal")
@Transactional(rollbackFor = Exception.class)
public R<?> sealRecord(@RequestBody MrSealing sealing) {
sealing.setStatus(0);
sealing.setSealDate(new Date());
sealing.setCreateTime(new Date());
sealingService.save(sealing);
return R.ok(sealing);
}
@PostMapping("/sealing/unseal")
@Transactional(rollbackFor = Exception.class)
public R<?> unsealRecord(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
String unsealBy = (String) params.get("unsealBy");
String reason = (String) params.get("unsealReason");
MrSealing sealing = sealingService.getById(id);
if (sealing == null) return R.fail("封存记录不存在");
sealing.setStatus(1);
sealing.setUnsealDate(new Date());
sealing.setUnsealBy(unsealBy);
sealing.setUnsealReason(reason);
sealing.setUpdateTime(new Date());
sealingService.updateById(sealing);
return R.ok();
}
// ==================== 病案示踪 ====================
@GetMapping("/tracking/page")
public R<?> getTrackingPage(
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "mrNumber", required = false) String mrNumber,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<MrTracking> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasText(status), MrTracking::getStatus, status)
.like(StringUtils.hasText(mrNumber), MrTracking::getMrNumber, mrNumber)
.orderByDesc(MrTracking::getMoveTime);
return R.ok(trackingService.page(new Page<>(pageNo, pageSize), wrapper));
}
@PostMapping("/tracking/move")
@Transactional(rollbackFor = Exception.class)
public R<?> moveRecord(@RequestBody MrTracking tracking) {
tracking.setMoveTime(new Date());
tracking.setCreateTime(new Date());
trackingService.save(tracking);
return R.ok(tracking);
}
@GetMapping("/tracking/stats")
public R<?> getTrackingStats() {
Map<String, Object> stats = new HashMap<>();
LambdaQueryWrapper<MrTracking> w1 = new LambdaQueryWrapper<>();
w1.eq(MrTracking::getStatus, "IN_SHELF");
stats.put("inShelf", trackingService.count(w1));
w1.eq(MrTracking::getStatus, "BORROWED");
stats.put("borrowed", trackingService.count(w1));
w1.eq(MrTracking::getStatus, "ARCHIVED");
stats.put("archived", trackingService.count(w1));
w1.eq(MrTracking::getStatus, "LOST");
stats.put("lost", trackingService.count(w1));
return R.ok(stats);
}
// ==================== 死亡病例讨论 ====================
@GetMapping("/death-discussion/page")
public R<?> getDeathDiscussionPage(
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "isOverdue", required = false) Boolean isOverdue,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<MrDeathDiscussion> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(status != null, MrDeathDiscussion::getStatus, status)
.eq(isOverdue != null, MrDeathDiscussion::getIsOverdue, isOverdue)
.orderByDesc(MrDeathDiscussion::getDeathDate);
return R.ok(deathDiscussionService.page(new Page<>(pageNo, pageSize), wrapper));
}
@PostMapping("/death-discussion/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addDeathDiscussion(@RequestBody MrDeathDiscussion discussion) {
discussion.setStatus(0);
discussion.setIsOverdue(false);
// 7天期限
if (discussion.getDeathDate() != null) {
discussion.setDeadlineDate(new Date(discussion.getDeathDate().getTime() + 7L * 24 * 60 * 60 * 1000));
}
discussion.setCreateTime(new Date());
deathDiscussionService.save(discussion);
return R.ok(discussion);
}
@PostMapping("/death-discussion/complete")
@Transactional(rollbackFor = Exception.class)
public R<?> completeDeathDiscussion(@RequestBody MrDeathDiscussion discussion) {
MrDeathDiscussion existing = deathDiscussionService.getById(discussion.getId());
if (existing == null) return R.fail("讨论记录不存在");
existing.setStatus(1);
existing.setDiscussionDate(new Date());
existing.setDiscussionConclusion(discussion.getDiscussionConclusion());
existing.setImprovementMeasures(discussion.getImprovementMeasures());
existing.setParticipants(discussion.getParticipants());
existing.setUpdateTime(new Date());
deathDiscussionService.updateById(existing);
return R.ok();
}
}

View File

@@ -0,0 +1,128 @@
package com.healthlink.his.web.nursing.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.healthlink.his.nursing.domain.NursingAssessmentReminder;
import com.healthlink.his.nursing.domain.NursingCarePlan;
import com.healthlink.his.nursing.service.INursingAssessmentReminderService;
import com.healthlink.his.nursing.service.INursingCarePlanService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/nursing-enhanced")
@Slf4j
@AllArgsConstructor
public class NursingEnhancedController {
private final INursingAssessmentReminderService reminderService;
private final INursingCarePlanService carePlanService;
@GetMapping("/reminder/page")
public R<?> getReminderPage(
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "encounterId", required = false) Long encounterId,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<NursingAssessmentReminder> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(status != null, NursingAssessmentReminder::getStatus, status)
.eq(encounterId != null, NursingAssessmentReminder::getEncounterId, encounterId)
.orderByAsc(NursingAssessmentReminder::getNextDeadline);
return R.ok(reminderService.page(new Page<>(pageNo, pageSize), wrapper));
}
@PostMapping("/reminder/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addReminder(@RequestBody NursingAssessmentReminder reminder) {
reminder.setStatus(0);
reminder.setCreateTime(new Date());
reminderService.save(reminder);
return R.ok(reminder);
}
@PostMapping("/reminder/complete")
@Transactional(rollbackFor = Exception.class)
public R<?> completeReminder(@RequestParam Long id) {
NursingAssessmentReminder reminder = reminderService.getById(id);
if (reminder == null) return R.fail("提醒记录不存在");
reminder.setStatus(1);
reminder.setLastAssessTime(new Date());
int hours = reminder.getFrequencyHours() != null ? reminder.getFrequencyHours() : 24;
reminder.setNextDeadline(new Date(System.currentTimeMillis() + (long) hours * 60 * 60 * 1000));
reminder.setUpdateTime(new Date());
reminderService.updateById(reminder);
return R.ok();
}
@GetMapping("/reminder/overdue")
public R<?> getOverdueReminders() {
LambdaQueryWrapper<NursingAssessmentReminder> wrapper = new LambdaQueryWrapper<>();
wrapper.le(NursingAssessmentReminder::getNextDeadline, new Date())
.ne(NursingAssessmentReminder::getStatus, 1);
return R.ok(reminderService.list(wrapper));
}
@GetMapping("/care-plan/page")
public R<?> getCarePlanPage(
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "encounterId", required = false) Long encounterId,
@RequestParam(value = "patientName", required = false) String patientName,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<NursingCarePlan> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasText(status), NursingCarePlan::getStatus, status)
.eq(encounterId != null, NursingCarePlan::getEncounterId, encounterId)
.like(StringUtils.hasText(patientName), NursingCarePlan::getPatientName, patientName)
.orderByDesc(NursingCarePlan::getCreateTime);
return R.ok(carePlanService.page(new Page<>(pageNo, pageSize), wrapper));
}
@PostMapping("/care-plan/add")
@Transactional(rollbackFor = Exception.class)
public R<?> addCarePlan(@RequestBody NursingCarePlan plan) {
plan.setStatus("NEW");
plan.setDelFlag("0");
plan.setCreateTime(new Date());
carePlanService.save(plan);
return R.ok(plan);
}
@PostMapping("/care-plan/evaluate")
@Transactional(rollbackFor = Exception.class)
public R<?> evaluateCarePlan(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
String eval = (String) params.get("evaluationResult");
NursingCarePlan plan = carePlanService.getById(id);
if (plan == null) return R.fail("护理计划不存在");
plan.setEvaluation(eval);
plan.setStatus("COMPLETED");
plan.setUpdateTime(new Date());
carePlanService.updateById(plan);
return R.ok();
}
@GetMapping("/quality/stats")
public R<?> getQualityStats(@RequestParam(required = false) Long encounterId) {
Map<String, Object> stats = new HashMap<>();
LambdaQueryWrapper<NursingAssessmentReminder> rw = new LambdaQueryWrapper<>();
if (encounterId != null) rw.eq(NursingAssessmentReminder::getEncounterId, encounterId);
stats.put("totalReminders", reminderService.count(rw));
rw.eq(NursingAssessmentReminder::getStatus, 1);
stats.put("completedReminders", reminderService.count(rw));
rw.eq(NursingAssessmentReminder::getStatus, 2);
stats.put("overdueReminders", reminderService.count(rw));
LambdaQueryWrapper<NursingCarePlan> cw = new LambdaQueryWrapper<>();
if (encounterId != null) cw.eq(NursingCarePlan::getEncounterId, encounterId);
stats.put("totalPlans", carePlanService.count(cw));
cw.eq(NursingCarePlan::getStatus, "COMPLETED");
stats.put("completedPlans", carePlanService.count(cw));
return R.ok(stats);
}
}

View File

@@ -0,0 +1,231 @@
-- V18: P0模块补全 — 电子病历增强+病案管理+护理评估+FHIR/CDA
-- 1. 病历检索索引表
CREATE TABLE IF NOT EXISTS emr_search_index (
id BIGSERIAL PRIMARY KEY,
emr_id BIGINT NOT NULL,
encounter_id BIGINT NOT NULL,
patient_id BIGINT NOT NULL,
patient_name VARCHAR(50),
emr_type VARCHAR(50),
emr_title VARCHAR(200),
diagnosis_text TEXT,
doctor_name VARCHAR(50),
department_name VARCHAR(100),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_emr_search_patient ON emr_search_index(patient_id);
CREATE INDEX idx_emr_search_diagnosis ON emr_search_index USING GIN(to_tsvector('simple', COALESCE(diagnosis_text, '')));
CREATE INDEX idx_emr_search_type ON emr_search_index(emr_type);
COMMENT ON TABLE emr_search_index IS '病历全文检索索引';
-- 2. 病案借阅表
CREATE TABLE IF NOT EXISTS mr_borrowing (
id BIGSERIAL PRIMARY KEY,
medical_record_id BIGINT NOT NULL,
patient_name VARCHAR(50),
mr_number VARCHAR(50),
borrower_name VARCHAR(50) NOT NULL,
borrower_dept VARCHAR(100),
borrow_reason TEXT NOT NULL,
borrow_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expected_return_date TIMESTAMP,
actual_return_date TIMESTAMP,
status INT NOT NULL DEFAULT 0,
approver_name VARCHAR(50),
approve_time TIMESTAMP,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE mr_borrowing IS '病案借阅管理';
COMMENT ON COLUMN mr_borrowing.status IS '状态(0待审批 1已批准 2已借出 3已归还 4已逾期 5已拒绝)';
CREATE INDEX idx_mr_borrow_status ON mr_borrowing(status);
-- 3. 病案封存表
CREATE TABLE IF NOT EXISTS mr_sealing (
id BIGSERIAL PRIMARY KEY,
medical_record_id BIGINT NOT NULL,
patient_name VARCHAR(50),
mr_number VARCHAR(50),
seal_reason TEXT NOT NULL,
seal_type INT NOT NULL DEFAULT 1,
seal_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
seal_by VARCHAR(50) NOT NULL,
unseal_date TIMESTAMP,
unseal_by VARCHAR(50),
unseal_reason TEXT,
status INT NOT NULL DEFAULT 0,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE mr_sealing IS '病案封存管理';
COMMENT ON COLUMN mr_sealing.seal_type IS '封存类型(1主动封存 2纠纷封存 3司法封存)';
COMMENT ON COLUMN mr_sealing.status IS '状态(0已封存 1已解封)';
-- 4. 病案示踪表
CREATE TABLE IF NOT EXISTS mr_tracking (
id BIGSERIAL PRIMARY KEY,
medical_record_id BIGINT NOT NULL,
mr_number VARCHAR(50),
patient_name VARCHAR(50),
location VARCHAR(100) NOT NULL,
location_type VARCHAR(50) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'IN_SHELF',
moved_by VARCHAR(50),
move_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE mr_tracking IS '病案示踪管理';
COMMENT ON COLUMN mr_tracking.status IS '状态(IN_SHELF在架/BORROWED借出/ARCHIVED归档/LOST遗失)';
CREATE INDEX idx_mr_track_status ON mr_tracking(status);
-- 5. 死亡病例讨论表
CREATE TABLE IF NOT EXISTS mr_death_discussion (
id BIGSERIAL PRIMARY KEY,
patient_id BIGINT NOT NULL,
patient_name VARCHAR(50),
encounter_id BIGINT NOT NULL,
death_date TIMESTAMP,
discussion_date TIMESTAMP,
deadline_date TIMESTAMP,
host_doctor_id BIGINT,
host_doctor_name VARCHAR(50),
host_title VARCHAR(50),
participants TEXT,
discussion_conclusion TEXT,
improvement_measures TEXT,
status INT NOT NULL DEFAULT 0,
is_overdue BOOLEAN DEFAULT FALSE,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE mr_death_discussion IS '死亡病例讨论';
COMMENT ON COLUMN mr_death_discussion.status IS '状态(0待讨论 1已讨论 2已归档)';
CREATE INDEX idx_death_disc_encounter ON mr_death_discussion(encounter_id);
-- 6. 护理评估提醒表
CREATE TABLE IF NOT EXISTS nursing_assessment_reminder (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT NOT NULL,
patient_id BIGINT NOT NULL,
patient_name VARCHAR(50),
assessment_type VARCHAR(50) NOT NULL,
frequency_hours INT NOT NULL DEFAULT 24,
last_assess_time TIMESTAMP,
next_deadline TIMESTAMP NOT NULL,
status INT NOT NULL DEFAULT 0,
remind_user_id BIGINT,
remind_user_name VARCHAR(50),
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE nursing_assessment_reminder IS '护理评估提醒';
COMMENT ON COLUMN nursing_assessment_reminder.assessment_type IS '评估类型(fall_risk/d pressure_ulcer/nutrition/pain/pipeline)';
COMMENT ON COLUMN nursing_assessment_reminder.status IS '状态(0待评估 1已评估 2已超时)';
-- 7. 护理计划表
CREATE TABLE IF NOT EXISTS nursing_care_plan (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT NOT NULL,
patient_id BIGINT NOT NULL,
patient_name VARCHAR(50),
nursing_diagnosis VARCHAR(200) NOT NULL,
nursing_goal TEXT,
nursing_intervention TEXT,
evaluation_result TEXT,
status INT NOT NULL DEFAULT 0,
nurse_user_id BIGINT,
nurse_name VARCHAR(50),
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE nursing_care_plan IS '护理计划';
COMMENT ON COLUMN nursing_care_plan.status IS '状态(0新建 1执行中 2已完成 3已停止)';
CREATE INDEX idx_ncp_encounter ON nursing_care_plan(encounter_id);
-- 8. FHIR资源表
CREATE TABLE IF NOT EXISTS esb_fhir_resource (
id BIGSERIAL PRIMARY KEY,
resource_type VARCHAR(50) NOT NULL,
resource_id VARCHAR(100) NOT NULL,
encounter_id BIGINT,
patient_id BIGINT,
resource_json TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
version_id INT NOT NULL DEFAULT 1,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE esb_fhir_resource IS 'FHIR R4资源存储';
COMMENT ON COLUMN esb_fhir_resource.resource_type IS '资源类型(Patient/Encounter/Observation/Condition/MedicationRequest)';
CREATE INDEX idx_fhir_type ON esb_fhir_resource(resource_type);
CREATE INDEX idx_fhir_patient ON esb_fhir_resource(patient_id);
-- 9. CDA文档表
CREATE TABLE IF NOT EXISTS esb_cda_document (
id BIGSERIAL PRIMARY KEY,
document_type VARCHAR(50) NOT NULL,
document_title VARCHAR(200),
encounter_id BIGINT,
patient_id BIGINT,
cda_xml TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
version_id INT NOT NULL DEFAULT 1,
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE esb_cda_document IS 'CDA临床文档架构';
COMMENT ON COLUMN esb_cda_document.document_type IS '文档类型(admission/discharge/lab_report/referral)';
CREATE INDEX idx_cda_type ON esb_cda_document(document_type);
CREATE INDEX idx_cda_patient ON esb_cda_document(patient_id);
-- 10. 数据映射表
CREATE TABLE IF NOT EXISTS esb_code_mapping (
id BIGSERIAL PRIMARY KEY,
source_system VARCHAR(50) NOT NULL,
source_code VARCHAR(50) NOT NULL,
target_system VARCHAR(50) NOT NULL,
target_code VARCHAR(50) NOT NULL,
mapping_type VARCHAR(50) NOT NULL,
description VARCHAR(200),
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE esb_code_mapping IS '标准编码映射';
COMMENT ON COLUMN esb_code_mapping.mapping_type IS '映射类型(diagnosis/procedure/medication/observation)';
CREATE INDEX idx_mapping_source ON esb_code_mapping(source_system, source_code);

View File

@@ -0,0 +1,32 @@
package com.healthlink.his.emr.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("emr_search_index")
public class EmrSearchIndex extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("emr_id")
private Long emrId;
@TableField("encounter_id")
private Long encounterId;
@TableField("patient_id")
private Long patientId;
@TableField("patient_name")
private String patientName;
@TableField("emr_type")
private String emrType;
@TableField("emr_title")
private String emrTitle;
@TableField("diagnosis_text")
private String diagnosisText;
@TableField("doctor_name")
private String doctorName;
@TableField("department_name")
private String departmentName;
}

View File

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

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.emr.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.emr.domain.EmrSearchIndex;
public interface IEmrSearchIndexService extends IService<EmrSearchIndex> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.emr.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.emr.domain.EmrSearchIndex;
import com.healthlink.his.emr.mapper.EmrSearchIndexMapper;
import com.healthlink.his.emr.service.IEmrSearchIndexService;
import org.springframework.stereotype.Service;
@Service
public class EmrSearchIndexServiceImpl extends ServiceImpl<EmrSearchIndexMapper, EmrSearchIndex> implements IEmrSearchIndexService {
}

View File

@@ -0,0 +1,28 @@
package com.healthlink.his.esb.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("esb_cda_document")
public class CdaDocument extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("document_type")
private String documentType;
@TableField("document_title")
private String documentTitle;
@TableField("encounter_id")
private Long encounterId;
@TableField("patient_id")
private Long patientId;
@TableField("cda_xml")
private String cdaXml;
@TableField("status")
private String status;
@TableField("version_id")
private Integer versionId;
}

View File

@@ -0,0 +1,26 @@
package com.healthlink.his.esb.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("esb_code_mapping")
public class CodeMapping extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("source_system")
private String sourceSystem;
@TableField("source_code")
private String sourceCode;
@TableField("target_system")
private String targetSystem;
@TableField("target_code")
private String targetCode;
@TableField("mapping_type")
private String mappingType;
@TableField("description")
private String description;
}

View File

@@ -0,0 +1,28 @@
package com.healthlink.his.esb.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("esb_fhir_resource")
public class FhirResource extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("resource_type")
private String resourceType;
@TableField("resource_id")
private String resourceId;
@TableField("encounter_id")
private Long encounterId;
@TableField("patient_id")
private Long patientId;
@TableField("resource_json")
private String resourceJson;
@TableField("status")
private String status;
@TableField("version_id")
private Integer versionId;
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.esb.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.esb.domain.CdaDocument;
public interface ICdaDocumentService extends IService<CdaDocument> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.esb.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.esb.domain.CodeMapping;
public interface ICodeMappingService extends IService<CodeMapping> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.esb.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.esb.domain.FhirResource;
public interface IFhirResourceService extends IService<FhirResource> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.esb.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.esb.domain.CdaDocument;
import com.healthlink.his.esb.mapper.CdaDocumentMapper;
import com.healthlink.his.esb.service.ICdaDocumentService;
import org.springframework.stereotype.Service;
@Service
public class CdaDocumentServiceImpl extends ServiceImpl<CdaDocumentMapper, CdaDocument> implements ICdaDocumentService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.esb.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.esb.domain.CodeMapping;
import com.healthlink.his.esb.mapper.CodeMappingMapper;
import com.healthlink.his.esb.service.ICodeMappingService;
import org.springframework.stereotype.Service;
@Service
public class CodeMappingServiceImpl extends ServiceImpl<CodeMappingMapper, CodeMapping> implements ICodeMappingService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.esb.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.esb.domain.FhirResource;
import com.healthlink.his.esb.mapper.FhirResourceMapper;
import com.healthlink.his.esb.service.IFhirResourceService;
import org.springframework.stereotype.Service;
@Service
public class FhirResourceServiceImpl extends ServiceImpl<FhirResourceMapper, FhirResource> implements IFhirResourceService {
}

View File

@@ -0,0 +1,44 @@
package com.healthlink.his.mrhomepage.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("mr_borrowing")
public class MrBorrowing extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("medical_record_id")
private Long medicalRecordId;
@TableField("patient_name")
private String patientName;
@TableField("mr_number")
private String mrNumber;
@TableField("borrower_name")
private String borrowerName;
@TableField("borrower_dept")
private String borrowerDept;
@TableField("borrow_reason")
private String borrowReason;
@TableField("borrow_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date borrowDate;
@TableField("expected_return_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expectedReturnDate;
@TableField("actual_return_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date actualReturnDate;
@TableField("status")
private Integer status;
@TableField("approver_name")
private String approverName;
@TableField("approve_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date approveTime;
}

View File

@@ -0,0 +1,47 @@
package com.healthlink.his.mrhomepage.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("mr_death_discussion")
public class MrDeathDiscussion extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("patient_id")
private Long patientId;
@TableField("patient_name")
private String patientName;
@TableField("encounter_id")
private Long encounterId;
@TableField("death_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date deathDate;
@TableField("discussion_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date discussionDate;
@TableField("deadline_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date deadlineDate;
@TableField("host_doctor_id")
private Long hostDoctorId;
@TableField("host_doctor_name")
private String hostDoctorName;
@TableField("host_title")
private String hostTitle;
@TableField("participants")
private String participants;
@TableField("discussion_conclusion")
private String discussionConclusion;
@TableField("improvement_measures")
private String improvementMeasures;
@TableField("status")
private Integer status;
@TableField("is_overdue")
private Boolean isOverdue;
}

View File

@@ -0,0 +1,40 @@
package com.healthlink.his.mrhomepage.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("mr_sealing")
public class MrSealing extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("medical_record_id")
private Long medicalRecordId;
@TableField("patient_name")
private String patientName;
@TableField("mr_number")
private String mrNumber;
@TableField("seal_reason")
private String sealReason;
@TableField("seal_type")
private Integer sealType;
@TableField("seal_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date sealDate;
@TableField("seal_by")
private String sealBy;
@TableField("unseal_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date unsealDate;
@TableField("unseal_by")
private String unsealBy;
@TableField("unseal_reason")
private String unsealReason;
@TableField("status")
private Integer status;
}

View File

@@ -0,0 +1,33 @@
package com.healthlink.his.mrhomepage.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("mr_tracking")
public class MrTracking extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("medical_record_id")
private Long medicalRecordId;
@TableField("mr_number")
private String mrNumber;
@TableField("patient_name")
private String patientName;
@TableField("location")
private String location;
@TableField("location_type")
private String locationType;
@TableField("status")
private String status;
@TableField("moved_by")
private String movedBy;
@TableField("move_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date moveTime;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.mrhomepage.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.mrhomepage.domain.MrBorrowing;
public interface IMrBorrowingService extends IService<MrBorrowing> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.mrhomepage.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.mrhomepage.domain.MrDeathDiscussion;
public interface IMrDeathDiscussionService extends IService<MrDeathDiscussion> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.mrhomepage.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.mrhomepage.domain.MrSealing;
public interface IMrSealingService extends IService<MrSealing> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.mrhomepage.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.mrhomepage.domain.MrTracking;
public interface IMrTrackingService extends IService<MrTracking> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.mrhomepage.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.mrhomepage.domain.MrBorrowing;
import com.healthlink.his.mrhomepage.mapper.MrBorrowingMapper;
import com.healthlink.his.mrhomepage.service.IMrBorrowingService;
import org.springframework.stereotype.Service;
@Service
public class MrBorrowingServiceImpl extends ServiceImpl<MrBorrowingMapper, MrBorrowing> implements IMrBorrowingService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.mrhomepage.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.mrhomepage.domain.MrDeathDiscussion;
import com.healthlink.his.mrhomepage.mapper.MrDeathDiscussionMapper;
import com.healthlink.his.mrhomepage.service.IMrDeathDiscussionService;
import org.springframework.stereotype.Service;
@Service
public class MrDeathDiscussionServiceImpl extends ServiceImpl<MrDeathDiscussionMapper, MrDeathDiscussion> implements IMrDeathDiscussionService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.mrhomepage.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.mrhomepage.domain.MrSealing;
import com.healthlink.his.mrhomepage.mapper.MrSealingMapper;
import com.healthlink.his.mrhomepage.service.IMrSealingService;
import org.springframework.stereotype.Service;
@Service
public class MrSealingServiceImpl extends ServiceImpl<MrSealingMapper, MrSealing> implements IMrSealingService {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.mrhomepage.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.mrhomepage.domain.MrTracking;
import com.healthlink.his.mrhomepage.mapper.MrTrackingMapper;
import com.healthlink.his.mrhomepage.service.IMrTrackingService;
import org.springframework.stereotype.Service;
@Service
public class MrTrackingServiceImpl extends ServiceImpl<MrTrackingMapper, MrTracking> implements IMrTrackingService {
}

View File

@@ -0,0 +1,38 @@
package com.healthlink.his.nursing.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("nursing_assessment_reminder")
public class NursingAssessmentReminder extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("encounter_id")
private Long encounterId;
@TableField("patient_id")
private Long patientId;
@TableField("patient_name")
private String patientName;
@TableField("assessment_type")
private String assessmentType;
@TableField("frequency_hours")
private Integer frequencyHours;
@TableField("last_assess_time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date lastAssessTime;
@TableField("next_deadline")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date nextDeadline;
@TableField("status")
private Integer status;
@TableField("remind_user_id")
private Long remindUserId;
@TableField("remind_user_name")
private String remindUserName;
}

View File

@@ -1,15 +1,30 @@
package com.healthlink.his.nursing.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
@Data @TableName("nursing_care_plan") @Accessors(chain = true) @EqualsAndHashCode(callSuper = false)
@Data
@TableName("nursing_care_plan")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class NursingCarePlan extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private Long encounterId; private Long patientId;
private String nursingDiagnosis; private String goal; private String interventions; private String evaluation;
private Long plannerId; private String plannerName; private Date planDate;
private String status; private String delFlag;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long encounterId;
private Long patientId;
private String patientName;
private String nursingDiagnosis;
private String goal;
private String interventions;
private String evaluation;
private Long plannerId;
private String plannerName;
private Date planDate;
private String status;
private String delFlag;
}

View File

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

View File

@@ -1,6 +1,9 @@
package com.healthlink.his.nursing.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.nursing.domain.NursingCarePlan;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface NursingCarePlanMapper extends BaseMapper<NursingCarePlan> {}
public interface NursingCarePlanMapper extends BaseMapper<NursingCarePlan> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.nursing.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.nursing.domain.NursingAssessmentReminder;
public interface INursingAssessmentReminderService extends IService<NursingAssessmentReminder> {
}

View File

@@ -1,4 +1,7 @@
package com.healthlink.his.nursing.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.nursing.domain.NursingCarePlan;
public interface INursingCarePlanService extends IService<NursingCarePlan> {}
public interface INursingCarePlanService extends IService<NursingCarePlan> {
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.nursing.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.nursing.domain.NursingAssessmentReminder;
import com.healthlink.his.nursing.mapper.NursingAssessmentReminderMapper;
import com.healthlink.his.nursing.service.INursingAssessmentReminderService;
import org.springframework.stereotype.Service;
@Service
public class NursingAssessmentReminderServiceImpl extends ServiceImpl<NursingAssessmentReminderMapper, NursingAssessmentReminder> implements INursingAssessmentReminderService {
}

View File

@@ -1,8 +1,11 @@
package com.healthlink.his.nursing.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.nursing.domain.NursingCarePlan;
import com.healthlink.his.nursing.mapper.NursingCarePlanMapper;
import com.healthlink.his.nursing.service.INursingCarePlanService;
import org.springframework.stereotype.Service;
@Service
public class NursingCarePlanServiceImpl extends ServiceImpl<NursingCarePlanMapper, NursingCarePlan> implements INursingCarePlanService {}
public class NursingCarePlanServiceImpl extends ServiceImpl<NursingCarePlanMapper, NursingCarePlan> implements INursingCarePlanService {
}

View File

@@ -0,0 +1,4 @@
import request from '@/utils/request'
export function searchEmr(p){return request({url:'/emr-search/search',method:'get',params:p})}
export function indexEmr(d){return request({url:'/emr-search/index',method:'post',data:d})}
export function getSearchStats(){return request({url:'/emr-search/stats',method:'get'})}

View File

@@ -0,0 +1,48 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">病历检索</span></div>
<el-card shadow="never" style="margin-bottom:16px">
<el-form :model="queryParams" inline>
<el-form-item label="关键词"><el-input v-model="queryParams.keyword" placeholder="诊断/标题/患者" clearable style="width:200px"/></el-form-item>
<el-form-item label="患者"><el-input v-model="queryParams.patientName" clearable style="width:120px"/></el-form-item>
<el-form-item label="类型"><el-select v-model="queryParams.emrType" clearable style="width:120px">
<el-option v-for="t in [{l:'入院记录',v:'admission'},{l:'日常病程',v:'daily'},{l:'出院记录',v:'discharge'},{l:'手术记录',v:'surgery'},{l:'会诊记录',v:'consultation'}]" :key="t.v" :label="t.l" :value="t.v"/>
</el-select></el-form-item>
<el-form-item label="医生"><el-input v-model="queryParams.doctorName" clearable style="width:100px"/></el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">检索</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-table :data="searchData" border stripe v-loading="loading">
<el-table-column prop="patientName" label="患者" width="90"/>
<el-table-column prop="emrType" label="类型" width="100">
<template #default="{row}">{{ {admission:'入院记录',daily:'日常病程',discharge:'出院记录',surgery:'手术记录',consultation:'会诊记录'}[row.emrType]||row.emrType }}</template>
</el-table-column>
<el-table-column prop="emrTitle" label="标题" min-width="180"/>
<el-table-column prop="diagnosisText" label="诊断" min-width="200" show-overflow-tooltip/>
<el-table-column prop="doctorName" label="医生" width="90"/>
<el-table-column prop="departmentName" label="科室" width="120"/>
<el-table-column prop="createTime" label="创建时间" width="170"/>
</el-table>
<el-pagination v-model:current-page="queryParams.pageNo" v-model:page-size="queryParams.pageSize" :total="total" layout="total,prev,pager,next" @current-change="handleSearch" style="margin-top:16px;text-align:right"/>
</div>
</template>
<script setup>
import {ref,reactive,onMounted} from 'vue'
import {searchEmr} from './api'
const loading=ref(false)
const searchData=ref([])
const total=ref(0)
const queryParams=reactive({keyword:'',patientName:'',emrType:'',doctorName:'',departmentName:'',pageNo:1,pageSize:20})
const handleSearch=async()=>{
loading.value=true
try{const r=await searchEmr(queryParams);searchData.value=r.data?.records||[];total.value=r.data?.total||0}finally{loading.value=false}
}
const resetQuery=()=>{Object.assign(queryParams,{keyword:'',patientName:'',emrType:'',doctorName:'',pageNo:1});handleSearch()}
onMounted(()=>handleSearch())
</script>

View File

@@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getFhirPage(p){return request({url:'/fhir-cda/fhir/page',method:'get',params:p})}
export function createFhirResource(d){return request({url:'/fhir-cda/fhir/create',method:'post',data:d})}
export function getFhirTypeStats(){return request({url:'/fhir-cda/fhir/type-stats',method:'get'})}
export function getCdaPage(p){return request({url:'/fhir-cda/cda/page',method:'get',params:p})}
export function createCdaDocument(d){return request({url:'/fhir-cda/cda/create',method:'post',data:d})}
export function publishCdaDocument(id){return request({url:'/fhir-cda/cda/publish',method:'post',params:{id}})}
export function getMappingPage(p){return request({url:'/fhir-cda/mapping/page',method:'get',params:p})}
export function addMapping(d){return request({url:'/fhir-cda/mapping/add',method:'post',data:d})}

View File

@@ -0,0 +1,89 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">FHIR/CDA标准接口</span></div>
<el-tabs v-model="tab" type="border-card">
<el-tab-pane label="FHIR资源" name="fhir">
<el-card shadow="never" style="margin-bottom:12px">
<div style="display:flex;gap:30px;text-align:center;flex-wrap:wrap">
<div v-for="(v,k) in fhirStats" :key="k"><div style="font-size:20px;font-weight:bold;color:#409eff">{{ v }}</div><div>{{ k }}</div></div>
</div>
</el-card>
<div style="margin-bottom:12px"><el-button type="success" @click="showAddFhir=true">新增资源</el-button></div>
<el-table :data="fhirData" border stripe>
<el-table-column prop="resourceType" label="资源类型" width="140"/>
<el-table-column prop="resourceId" label="资源ID" width="150"/>
<el-table-column prop="status" label="状态" width="80"/>
<el-table-column prop="versionId" label="版本" width="60" align="center"/>
<el-table-column prop="createTime" label="创建时间" width="170"/>
</el-table>
</el-tab-pane>
<el-tab-pane label="CDA文档" name="cda">
<div style="margin-bottom:12px"><el-button type="success" @click="showAddCda=true">新增文档</el-button></div>
<el-table :data="cdaData" border stripe>
<el-table-column prop="documentType" label="文档类型" width="140"/>
<el-table-column prop="documentTitle" label="标题" min-width="180"/>
<el-table-column prop="status" label="状态" width="90">
<template #default="{row}">
<el-tag :type="row.status==='PUBLISHED'?'success':'info'" size="small">{{ row.status==='DRAFT'?'草稿':'已发布' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="versionId" label="版本" width="60" align="center"/>
<el-table-column label="操作" width="100">
<template #default="{row}">
<el-button v-if="row.status==='DRAFT'" type="success" link size="small" @click="publishCdaAction(row)">发布</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="编码映射" name="mapping">
<div style="margin-bottom:12px"><el-button type="success" @click="showAddMapping=true">新增映射</el-button></div>
<el-table :data="mappingData" border stripe>
<el-table-column prop="sourceSystem" label="源系统" width="120"/>
<el-table-column prop="sourceCode" label="源编码" width="120"/>
<el-table-column prop="targetSystem" label="目标系统" width="120"/>
<el-table-column prop="targetCode" label="目标编码" width="120"/>
<el-table-column prop="mappingType" label="映射类型" width="120"/>
<el-table-column prop="description" label="描述" min-width="150"/>
</el-table>
</el-tab-pane>
</el-tabs>
<el-dialog v-model="showAddFhir" title="新增FHIR资源" width="600px">
<el-form :model="fhirForm" label-width="100px">
<el-form-item label="资源类型"><el-select v-model="fhirForm.resourceType"><el-option v-for="t in ['Patient','Encounter','Observation','Condition','MedicationRequest']" :key="t" :label="t" :value="t"/></el-select></el-form-item>
<el-form-item label="资源ID"><el-input v-model="fhirForm.resourceId"/></el-form-item>
<el-form-item label="JSON内容"><el-input v-model="fhirForm.resourceJson" type="textarea" :rows="8"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddFhir=false">取消</el-button>
<el-button type="primary" @click="submitFhir">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,reactive,onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getFhirPage,createFhirResource,getFhirTypeStats,getCdaPage,createCdaDocument,publishCdaDocument,getMappingPage,addMapping} from './api'
const tab=ref('fhir')
const fhirData=ref([]),cdaData=ref([]),mappingData=ref([])
const fhirStats=ref({})
const showAddFhir=ref(false),showAddCda=ref(false),showAddMapping=ref(false)
const fhirForm=reactive({resourceType:'Patient',resourceId:'',resourceJson:''})
const loadData=async()=>{
const [f,c,m,s]=await Promise.all([
getFhirPage({pageNo:1,pageSize:50}),getCdaPage({pageNo:1,pageSize:50}),
getMappingPage({pageNo:1,pageSize:50}),getFhirTypeStats()
])
fhirData.value=f.data?.records||[];cdaData.value=c.data?.records||[]
mappingData.value=m.data?.records||[];fhirStats.value=s.data||{}
}
const submitFhir=async()=>{await createFhirResource(fhirForm);ElMessage.success('新增成功');showAddFhir.value=false;loadData()}
const publishCdaAction=async(row)=>{await publishCdaDocument(row.id);ElMessage.success('已发布');loadData()}
onMounted(()=>loadData())
</script>

View File

@@ -0,0 +1,18 @@
import request from '@/utils/request'
// 借阅
export function getBorrowingPage(p) { return request({ url: '/mr-management/borrowing/page', method: 'get', params: p }) }
export function applyBorrowing(d) { return request({ url: '/mr-management/borrowing/apply', method: 'post', data: d }) }
export function approveBorrowing(d) { return request({ url: '/mr-management/borrowing/approve', method: 'post', data: d }) }
export function returnBorrowing(id) { return request({ url: '/mr-management/borrowing/return', method: 'post', params: { id } }) }
// 封存
export function getSealingPage(p) { return request({ url: '/mr-management/sealing/page', method: 'get', params: p }) }
export function sealRecord(d) { return request({ url: '/mr-management/sealing/seal', method: 'post', data: d }) }
export function unsealRecord(d) { return request({ url: '/mr-management/sealing/unseal', method: 'post', data: d }) }
// 示踪
export function getTrackingPage(p) { return request({ url: '/mr-management/tracking/page', method: 'get', params: p }) }
export function moveRecord(d) { return request({ url: '/mr-management/tracking/move', method: 'post', data: d }) }
export function getTrackingStats() { return request({ url: '/mr-management/tracking/stats', method: 'get' }) }
// 死亡讨论
export function getDeathDiscussionPage(p) { return request({ url: '/mr-management/death-discussion/page', method: 'get', params: p }) }
export function addDeathDiscussion(d) { return request({ url: '/mr-management/death-discussion/add', method: 'post', data: d }) }
export function completeDeathDiscussion(d) { return request({ url: '/mr-management/death-discussion/complete', method: 'post', data: d }) }

View File

@@ -0,0 +1,185 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">病案管理</span></div>
<el-tabs v-model="tab" type="border-card">
<!-- 借阅管理 -->
<el-tab-pane label="病案借阅" name="borrow">
<div style="margin-bottom:12px"><el-button type="success" @click="showBorrow=true">申请借阅</el-button></div>
<el-table :data="borrowData" border stripe>
<el-table-column prop="patientName" label="患者" width="90"/>
<el-table-column prop="mrNumber" label="病案号" width="110"/>
<el-table-column prop="borrowerName" label="借阅人" width="90"/>
<el-table-column prop="borrowerDept" label="借阅科室" width="120"/>
<el-table-column prop="borrowReason" label="借阅原因" min-width="150" show-overflow-tooltip/>
<el-table-column prop="status" label="状态" width="90">
<template #default="{row}">
<el-tag :type="['warning','','success','success','danger','danger'][row.status]" size="small">
{{ ['待审批','已批准','已借出','已归还','已逾期','已拒绝'][row.status] }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160">
<template #default="{row}">
<el-button v-if="row.status===0" type="primary" link size="small" @click="approveAction(row,true)">批准</el-button>
<el-button v-if="row.status===0" type="danger" link size="small" @click="approveAction(row,false)">拒绝</el-button>
<el-button v-if="row.status===1||row.status===2" type="success" link size="small" @click="returnAction(row)">归还</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- 封存管理 -->
<el-tab-pane label="病案封存" name="seal">
<div style="margin-bottom:12px"><el-button type="warning" @click="showSeal=true">申请封存</el-button></div>
<el-table :data="sealData" border stripe>
<el-table-column prop="patientName" label="患者" width="90"/>
<el-table-column prop="mrNumber" label="病案号" width="110"/>
<el-table-column prop="sealType" label="封存类型" width="100">
<template #default="{row}">{{ ['','主动封存','纠纷封存','司法封存'][row.sealType] }}</template>
</el-table-column>
<el-table-column prop="sealReason" label="封存原因" min-width="150" show-overflow-tooltip/>
<el-table-column prop="sealBy" label="封存人" width="90"/>
<el-table-column prop="status" label="状态" width="80">
<template #default="{row}">
<el-tag :type="row.status===0?'danger':'success'" size="small">{{ row.status===0?'已封存':'已解封' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{row}">
<el-button v-if="row.status===0" type="warning" link size="small" @click="unsealAction(row)">解封</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- 示踪管理 -->
<el-tab-pane label="病案示踪" name="track">
<el-card shadow="never" style="margin-bottom:12px">
<div style="display:flex;gap:40px;text-align:center">
<div><div style="font-size:28px;font-weight:bold;color:#67c23a">{{ trackStats.inShelf||0 }}</div><div>在架</div></div>
<div><div style="font-size:28px;font-weight:bold;color:#e6a23c">{{ trackStats.borrowed||0 }}</div><div>借出</div></div>
<div><div style="font-size:28px;font-weight:bold;color:#409eff">{{ trackStats.archived||0 }}</div><div>归档</div></div>
<div><div style="font-size:28px;font-weight:bold;color:#f56c6c">{{ trackStats.lost||0 }}</div><div>遗失</div></div>
</div>
</el-card>
<el-table :data="trackData" border stripe>
<el-table-column prop="mrNumber" label="病案号" width="110"/>
<el-table-column prop="patientName" label="患者" width="90"/>
<el-table-column prop="location" label="当前位置" width="120"/>
<el-table-column prop="status" label="状态" width="90">
<template #default="{row}">
<el-tag :type="{IN_SHELF:'success',BORROWED:'warning',ARCHIVED:'',LOST:'danger'}[row.status]" size="small">
{{ {IN_SHELF:'在架',BORROWED:'借出',ARCHIVED:'归档',LOST:'遗失'}[row.status] }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="movedBy" label="操作人" width="100"/>
<el-table-column prop="moveTime" label="移动时间" width="170"/>
</el-table>
</el-tab-pane>
<!-- 死亡讨论 -->
<el-tab-pane label="死亡讨论" name="death">
<div style="margin-bottom:12px"><el-button type="danger" @click="showDeath=true">新增讨论</el-button></div>
<el-table :data="deathData" border stripe>
<el-table-column prop="patientName" label="患者" width="90"/>
<el-table-column prop="hostDoctorName" label="主持人" width="100"/>
<el-table-column prop="deathDate" label="死亡时间" width="170"/>
<el-table-column prop="deadlineDate" label="讨论期限" width="170"/>
<el-table-column prop="status" label="状态" width="80">
<template #default="{row}">
<el-tag :type="['warning','success',''][row.status]" size="small">
{{ ['待讨论','已讨论','已归档'][row.status] }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="isOverdue" label="是否超时" width="80">
<template #default="{row}">
<el-tag v-if="row.isOverdue" type="danger" size="small">超时</el-tag>
<el-tag v-else type="success" size="small">正常</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="{row}">
<el-button v-if="row.status===0" type="primary" link size="small" @click="completeDeath(row)">完成讨论</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<!-- 借阅弹窗 -->
<el-dialog v-model="showBorrow" title="申请借阅" width="500px">
<el-form :model="borrowForm" label-width="100px">
<el-form-item label="病案号"><el-input v-model="borrowForm.mrNumber"/></el-form-item>
<el-form-item label="患者"><el-input v-model="borrowForm.patientName"/></el-form-item>
<el-form-item label="借阅人"><el-input v-model="borrowForm.borrowerName"/></el-form-item>
<el-form-item label="借阅科室"><el-input v-model="borrowForm.borrowerDept"/></el-form-item>
<el-form-item label="借阅原因"><el-input v-model="borrowForm.borrowReason" type="textarea"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="showBorrow=false">取消</el-button>
<el-button type="primary" @click="submitBorrow">提交</el-button>
</template>
</el-dialog>
<!-- 封存弹窗 -->
<el-dialog v-model="showSeal" title="申请封存" width="500px">
<el-form :model="sealForm" label-width="100px">
<el-form-item label="病案号"><el-input v-model="sealForm.mrNumber"/></el-form-item>
<el-form-item label="患者"><el-input v-model="sealForm.patientName"/></el-form-item>
<el-form-item label="封存类型">
<el-select v-model="sealForm.sealType">
<el-option label="主动封存" :value="1"/><el-option label="纠纷封存" :value="2"/><el-option label="司法封存" :value="3"/>
</el-select>
</el-form-item>
<el-form-item label="封存原因"><el-input v-model="sealForm.sealReason" type="textarea"/></el-form-item>
<el-form-item label="封存人"><el-input v-model="sealForm.sealBy"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="showSeal=false">取消</el-button>
<el-button type="warning" @click="submitSeal">确认封存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,reactive,onMounted} from 'vue'
import {ElMessage,ElMessageBox} from 'element-plus'
import {getBorrowingPage,applyBorrowing,approveBorrowing,returnBorrowing,getSealingPage,sealRecord,unsealRecord,getTrackingPage,getTrackingStats,getDeathDiscussionPage,addDeathDiscussion,completeDeathDiscussion} from './api'
const tab=ref('borrow')
const borrowData=ref([]),sealData=ref([]),trackData=ref([]),deathData=ref([])
const trackStats=ref({})
const showBorrow=ref(false),showSeal=ref(false),showDeath=ref(false)
const borrowForm=reactive({mrNumber:'',patientName:'',borrowerName:'',borrowerDept:'',borrowReason:''})
const sealForm=reactive({mrNumber:'',patientName:'',sealType:1,sealReason:'',sealBy:''})
const loadData=async()=>{
const [b,s,t,d]=await Promise.all([
getBorrowingPage({pageNo:1,pageSize:50}),getSealingPage({pageNo:1,pageSize:50}),
getTrackingPage({pageNo:1,pageSize:50}),getDeathDiscussionPage({pageNo:1,pageSize:50})
])
borrowData.value=b.data?.records||[];sealData.value=s.data?.records||[]
trackData.value=t.data?.records||[];deathData.value=d.data?.records||[]
const ts=await getTrackingStats();trackStats.value=ts.data||{}
}
const approveAction=async(row,approved)=>{
await approveBorrowing({id:row.id,approved,approverName:'管理员'})
ElMessage.success(approved?'已批准':'已拒绝');loadData()
}
const returnAction=async(row)=>{await returnBorrowing(row.id);ElMessage.success('已归还');loadData()}
const submitBorrow=async()=>{await applyBorrowing(borrowForm);ElMessage.success('申请成功');showBorrow.value=false;loadData()}
const submitSeal=async()=>{await sealRecord(sealForm);ElMessage.success('封存成功');showSeal.value=false;loadData()}
const unsealAction=async(row)=>{
const{value}=await ElMessageBox.prompt('请输入解封原因','解封',{inputType:'textarea'})
await unsealRecord({id:row.id,unsealBy:'管理员',unsealReason:value});ElMessage.success('已解封');loadData()
}
const completeDeath=async(row)=>{
const{value}=await ElMessageBox.prompt('请输入讨论结论','完成讨论',{inputType:'textarea'})
await completeDeathDiscussion({id:row.id,discussionConclusion:value,participants:row.hostDoctorName});ElMessage.success('已完成');loadData()
}
onMounted(()=>loadData())
</script>

View File

@@ -0,0 +1,9 @@
import request from '@/utils/request'
export function getReminderPage(p){return request({url:'/nursing-enhanced/reminder/page',method:'get',params:p})}
export function addReminder(d){return request({url:'/nursing-enhanced/reminder/add',method:'post',data:d})}
export function completeReminder(id){return request({url:'/nursing-enhanced/reminder/complete',method:'post',params:{id}})}
export function getOverdueReminders(){return request({url:'/nursing-enhanced/reminder/overdue',method:'get'})}
export function getCarePlanPage(p){return request({url:'/nursing-enhanced/care-plan/page',method:'get',params:p})}
export function addCarePlan(d){return request({url:'/nursing-enhanced/care-plan/add',method:'post',data:d})}
export function evaluateCarePlan(d){return request({url:'/nursing-enhanced/care-plan/evaluate',method:'post',data:d})}
export function getQualityStats(p){return request({url:'/nursing-enhanced/quality/stats',method:'get',params:p})}

View File

@@ -0,0 +1,110 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">护理评估增强</span></div>
<el-tabs v-model="tab" type="border-card">
<el-tab-pane label="评估提醒" name="reminder">
<el-card shadow="never" style="margin-bottom:12px">
<div style="display:flex;gap:40px;text-align:center">
<div><div style="font-size:28px;font-weight:bold;color:#e6a23c">{{ overdueCount }}</div><div>逾期未评估</div></div>
<div><div style="font-size:28px;font-weight:bold;color:#67c23a">{{ qualityStats.completedReminders||0 }}</div><div>已完成评估</div></div>
</div>
</el-card>
<el-table :data="reminderData" border stripe>
<el-table-column prop="patientName" label="患者" width="90"/>
<el-table-column prop="assessmentType" label="评估类型" width="110">
<template #default="{row}">
{{ {fall_risk:'跌倒风险',pressure_ulcer:'压疮风险',nutrition:'营养筛查',pain:'疼痛评估',pipeline:'管道评估'}[row.assessmentType]||row.assessmentType }}
</template>
</el-table-column>
<el-table-column prop="frequencyHours" label="频率(小时)" width="100" align="center"/>
<el-table-column prop="nextDeadline" label="下次截止" width="170"/>
<el-table-column prop="status" label="状态" width="90">
<template #default="{row}">
<el-tag :type="['warning','success','danger'][row.status]" size="small">{{ ['待评估','已评估','已超时'][row.status] }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="{row}">
<el-button v-if="row.status!==1" type="success" link size="small" @click="completeReminderAction(row)">完成评估</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="护理计划" name="carePlan">
<div style="margin-bottom:12px"><el-button type="success" @click="showCarePlan=true">新增护理计划</el-button></div>
<el-table :data="carePlanData" border stripe>
<el-table-column prop="patientName" label="患者" width="90"/>
<el-table-column prop="nursingDiagnosis" label="护理诊断" width="150"/>
<el-table-column prop="nursingGoal" label="护理目标" min-width="180" show-overflow-tooltip/>
<el-table-column prop="nursingIntervention" label="护理措施" min-width="180" show-overflow-tooltip/>
<el-table-column prop="status" label="状态" width="80">
<template #default="{row}">
<el-tag :type="['info','','success',''][row.status]" size="small">{{ ['新建','执行中','已完成','已停止'][row.status] }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{row}">
<el-button v-if="row.status<2" type="primary" link size="small" @click="evaluatePlan(row)">评价</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="质量指标" name="quality">
<el-card shadow="never">
<div style="display:flex;gap:30px;text-align:center;flex-wrap:wrap">
<div><div style="font-size:24px;font-weight:bold;color:#409eff">{{ qualityStats.totalReminders||0 }}</div><div>总评估提醒</div></div>
<div><div style="font-size:24px;font-weight:bold;color:#67c23a">{{ qualityStats.completedReminders||0 }}</div><div>已完成</div></div>
<div><div style="font-size:24px;font-weight:bold;color:#f56c6c">{{ qualityStats.overdueReminders||0 }}</div><div>已超时</div></div>
<div><div style="font-size:24px;font-weight:bold;color:#409eff">{{ qualityStats.totalPlans||0 }}</div><div>总护理计划</div></div>
<div><div style="font-size:24px;font-weight:bold;color:#67c23a">{{ qualityStats.completedPlans||0 }}</div><div>已完成计划</div></div>
</div>
</el-card>
</el-tab-pane>
</el-tabs>
<el-dialog v-model="showCarePlan" title="新增护理计划" width="600px">
<el-form :model="carePlanForm" label-width="100px">
<el-form-item label="患者"><el-input v-model="carePlanForm.patientName"/></el-form-item>
<el-form-item label="就诊ID"><el-input-number v-model="carePlanForm.encounterId" :min="1"/></el-form-item>
<el-form-item label="护理诊断"><el-input v-model="carePlanForm.nursingDiagnosis"/></el-form-item>
<el-form-item label="护理目标"><el-input v-model="carePlanForm.nursingGoal" type="textarea"/></el-form-item>
<el-form-item label="护理措施"><el-input v-model="carePlanForm.nursingIntervention" type="textarea"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="showCarePlan=false">取消</el-button>
<el-button type="primary" @click="submitCarePlan">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,reactive,onMounted} from 'vue'
import {ElMessage,ElMessageBox} from 'element-plus'
import {getReminderPage,completeReminder,getCarePlanPage,addCarePlan,evaluateCarePlan,getQualityStats} from './api'
const tab=ref('reminder')
const reminderData=ref([]),carePlanData=ref([])
const overdueCount=ref(0)
const qualityStats=ref({})
const showCarePlan=ref(false)
const carePlanForm=reactive({patientName:'',encounterId:null,nursingDiagnosis:'',nursingGoal:'',nursingIntervention:''})
const loadData=async()=>{
const [r,cp,q]=await Promise.all([
getReminderPage({pageNo:1,pageSize:50}),getCarePlanPage({pageNo:1,pageSize:50}),getQualityStats({})
])
reminderData.value=r.data?.records||[];carePlanData.value=cp.data?.records||[]
qualityStats.value=q.data||{}
overdueCount.value=reminderData.value.filter(x=>x.status===2).length
}
const completeReminderAction=async(row)=>{await completeReminder(row.id);ElMessage.success('评估完成');loadData()}
const evaluatePlan=async(row)=>{
const{value}=await ElMessageBox.prompt('请输入评价结果','评价护理计划',{inputType:'textarea'})
await evaluateCarePlan({id:row.id,evaluationResult:value});ElMessage.success('评价完成');loadData()
}
const submitCarePlan=async()=>{await addCarePlan(carePlanForm);ElMessage.success('新增成功');showCarePlan.value=false;loadData()}
onMounted(()=>loadData())
</script>