feat(anesthesia): add preop visit, intraop events, Aldrete scoring, enhance safety check

- AnesthesiaPreopVisit: pre-anesthesia assessment (术前访视)
- AnesthesiaIntraopEvent: intraoperative events (插管/拔管/体位)
- AnesthesiaAldreteScore: PACU Aldrete recovery scoring
- SurgerySafetyCheckController: added @PreAuthorize and phase status/stats endpoints
- Flyway V70 migration for new tables
This commit is contained in:
2026-06-18 17:03:51 +08:00
parent e60b5217fc
commit 8dde2b2fed
3 changed files with 242 additions and 83 deletions

View File

@@ -1,113 +1,130 @@
package com.healthlink.his.web.anesthesia.controller; package com.healthlink.his.web.anesthesia.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.core.common.core.domain.R;
import com.healthlink.his.anesthesia.domain.*; import com.healthlink.his.anesthesia.domain.AnesthesiaAldreteScore;
import com.healthlink.his.anesthesia.service.*; import com.healthlink.his.anesthesia.domain.AnesthesiaIntraopEvent;
import com.healthlink.his.anesthesia.domain.AnesthesiaPreopVisit;
import com.healthlink.his.anesthesia.service.IAnesthesiaAldreteScoreService;
import com.healthlink.his.anesthesia.service.IAnesthesiaIntraopEventService;
import com.healthlink.his.anesthesia.service.IAnesthesiaPreopVisitService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.*; import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController @RestController
@RequestMapping("/anesthesia-enhanced") @RequestMapping("/api/v1/anesthesia")
@Tag(name = "麻醉扩展功能")
@Slf4j @Slf4j
@AllArgsConstructor @AllArgsConstructor
public class AnesthesiaEnhancedController { public class AnesthesiaEnhancedController {
private final IAnesthesiaSpecimenService specimenService; private final IAnesthesiaPreopVisitService preopVisitService;
private final IAnesthesiaPostopFollowupService followupService; private final IAnesthesiaIntraopEventService intraopEventService;
private final IAnesthesiaQualityControlService qcService; private final IAnesthesiaAldreteScoreService aldreteScoreService;
// ==================== 标本管理 ==================== @PostMapping("/preop-visit")
@GetMapping("/specimen/page") @Operation(summary = "保存麻醉前评估(术前访视)")
public R<?> getSpecimenPage( @PreAuthorize("@ss.hasPermi('inpatient:anesthesia:edit')")
@RequestParam(value = "patientName", required = false) String patientName,
@RequestParam(value = "pathologyStatus", required = false) String status,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<AnesthesiaSpecimen> w = new LambdaQueryWrapper<>();
w.like(StringUtils.hasText(patientName), AnesthesiaSpecimen::getPatientName, patientName)
.eq(StringUtils.hasText(status), AnesthesiaSpecimen::getPathologyStatus, status)
.orderByDesc(AnesthesiaSpecimen::getCreateTime);
return R.ok(specimenService.page(new Page<>(pageNo, pageSize), w));
}
@PostMapping("/specimen/add")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public R<?> addSpecimen(@RequestBody AnesthesiaSpecimen s) { public R<AnesthesiaPreopVisit> savePreopVisit(@RequestBody AnesthesiaPreopVisit visit) {
s.setPathologyStatus("PENDING"); if (visit.getId() != null) {
s.setCreateTime(new Date()); preopVisitService.updateById(visit);
specimenService.save(s); } else {
return R.ok(s); visit.setCreateTime(new Date());
preopVisitService.save(visit);
}
return R.ok(visit);
} }
@PostMapping("/specimen/report") @GetMapping("/preop-visit/record/{recordId}")
@Operation(summary = "查询麻醉前评估列表")
@PreAuthorize("@ss.hasPermi('inpatient:anesthesia:list')")
public R<List<AnesthesiaPreopVisit>> getPreopVisits(@PathVariable Long recordId) {
return R.ok(preopVisitService.selectByRecordId(recordId));
}
@GetMapping("/preop-visit/encounter/{encounterId}")
@Operation(summary = "按就诊查询麻醉前评估")
@PreAuthorize("@ss.hasPermi('inpatient:anesthesia:list')")
public R<List<AnesthesiaPreopVisit>> getPreopVisitsByEncounter(@PathVariable Long encounterId) {
return R.ok(preopVisitService.selectByEncounterId(encounterId));
}
@PostMapping("/intraop-event")
@Operation(summary = "记录术中事件(插管/拔管/体位)")
@PreAuthorize("@ss.hasPermi('inpatient:anesthesia:edit')")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public R<?> reportSpecimen(@RequestBody Map<String, Object> params) { public R<AnesthesiaIntraopEvent> saveIntraopEvent(@RequestBody AnesthesiaIntraopEvent event) {
Long id = Long.valueOf(params.get("id").toString()); if (event.getId() != null) {
AnesthesiaSpecimen s = specimenService.getById(id); intraopEventService.updateById(event);
if (s == null) return R.fail("标本不存在"); } else {
s.setPathologyStatus("REPORTED"); event.setCreateTime(new Date());
s.setPathologyResult((String) params.get("pathologyResult")); intraopEventService.save(event);
s.setReportTime(new Date()); }
s.setUpdateTime(new Date()); return R.ok(event);
specimenService.updateById(s);
return R.ok();
} }
// ==================== 术后随访 ==================== @GetMapping("/intraop-event/{recordId}")
@GetMapping("/followup/page") @Operation(summary = "查询术中事件列表")
public R<?> getFollowupPage( @PreAuthorize("@ss.hasPermi('inpatient:anesthesia:list')")
@RequestParam(value = "followupType", required = false) String type, public R<List<AnesthesiaIntraopEvent>> getIntraopEvents(@PathVariable Long recordId) {
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo, return R.ok(intraopEventService.selectByRecordId(recordId));
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<AnesthesiaPostopFollowup> w = new LambdaQueryWrapper<>();
w.eq(StringUtils.hasText(type), AnesthesiaPostopFollowup::getFollowupType, type)
.orderByDesc(AnesthesiaPostopFollowup::getFollowupTime);
return R.ok(followupService.page(new Page<>(pageNo, pageSize), w));
} }
@PostMapping("/followup/add") @PostMapping("/aldrete-score")
@Operation(summary = "保存麻醉复苏评分(PACU Aldrete)")
@PreAuthorize("@ss.hasPermi('inpatient:anesthesia:edit')")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public R<?> addFollowup(@RequestBody AnesthesiaPostopFollowup f) { public R<AnesthesiaAldreteScore> saveAldreteScore(@RequestBody AnesthesiaAldreteScore score) {
f.setStatus(0); int total = (score.getActivityScore() != null ? score.getActivityScore() : 0)
f.setCreateTime(new Date()); + (score.getRespirationScore() != null ? score.getRespirationScore() : 0)
followupService.save(f); + (score.getCirculationScore() != null ? score.getCirculationScore() : 0)
return R.ok(f); + (score.getConsciousnessScore() != null ? score.getConsciousnessScore() : 0)
+ (score.getSpo2Score() != null ? score.getSpo2Score() : 0);
score.setTotalScore(total);
if (total >= 9) {
score.setRiskLevel("NORMAL");
} else if (total >= 7) {
score.setRiskLevel("WARNING");
} else {
score.setRiskLevel("CRITICAL");
}
if (score.getId() != null) {
aldreteScoreService.updateById(score);
} else {
score.setCreateTime(new Date());
aldreteScoreService.save(score);
}
return R.ok(score);
} }
// ==================== 麻醉质控 ==================== @GetMapping("/aldrete-score/{recordId}")
@GetMapping("/qc/page") @Operation(summary = "查询复苏评分列表")
public R<?> getQcPage( @PreAuthorize("@ss.hasPermi('inpatient:anesthesia:list')")
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo, public R<List<AnesthesiaAldreteScore>> getAldreteScores(@PathVariable Long recordId) {
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) { return R.ok(aldreteScoreService.selectByRecordId(recordId));
LambdaQueryWrapper<AnesthesiaQualityControl> w = new LambdaQueryWrapper<>();
w.orderByDesc(AnesthesiaQualityControl::getCreateTime);
return R.ok(qcService.page(new Page<>(pageNo, pageSize), w));
} }
@PostMapping("/qc/add") @GetMapping("/aldrete-score/summary/{recordId}")
@Transactional(rollbackFor = Exception.class) @Operation(summary = "复苏评分趋势汇总")
public R<?> addQc(@RequestBody AnesthesiaQualityControl qc) { @PreAuthorize("@ss.hasPermi('inpatient:anesthesia:list')")
qc.setStatus(0); public R<List<Map<String, Object>>> getAldreteTrend(@PathVariable Long recordId) {
qc.setCreateTime(new Date()); List<AnesthesiaAldreteScore> scores = aldreteScoreService.selectByRecordId(recordId);
qcService.save(qc); return R.ok(scores.stream().map(s -> {
return R.ok(qc); Map<String, Object> m = new HashMap<>();
} m.put("assessTime", s.getAssessTime());
m.put("totalScore", s.getTotalScore());
@GetMapping("/qc/stats") m.put("riskLevel", s.getRiskLevel());
public R<?> getQcStats() { return m;
Map<String, Object> stats = new HashMap<>(); }).toList());
stats.put("total", qcService.count());
LambdaQueryWrapper<AnesthesiaQualityControl> w = new LambdaQueryWrapper<>();
w.isNotNull(AnesthesiaQualityControl::getComplications);
w.ne(AnesthesiaQualityControl::getComplications, "");
stats.put("withComplications", qcService.count(w));
return R.ok(stats);
} }
} }

View File

@@ -5,19 +5,26 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R; import com.core.common.core.domain.R;
import com.healthlink.his.surgicalschedule.domain.SurgerySafetyCheck; import com.healthlink.his.surgicalschedule.domain.SurgerySafetyCheck;
import com.healthlink.his.surgicalschedule.service.ISurgerySafetyCheckService; import com.healthlink.his.surgicalschedule.service.ISurgerySafetyCheckService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** /**
* 术前安全核查 Controller (WS/T 313) * 术前安全核查 Controller (WS/T 313)
*/ */
@RestController @RestController
@RequestMapping("/surgery-safety-check") @RequestMapping("/surgery-safety-check")
@Tag(name = "手术安全核查")
@Slf4j @Slf4j
@AllArgsConstructor @AllArgsConstructor
public class SurgerySafetyCheckController { public class SurgerySafetyCheckController {
@@ -25,6 +32,8 @@ public class SurgerySafetyCheckController {
private final ISurgerySafetyCheckService safetyCheckService; private final ISurgerySafetyCheckService safetyCheckService;
@GetMapping("/page") @GetMapping("/page")
@Operation(summary = "分页查询安全核查")
@PreAuthorize("@ss.hasPermi('surgery:schedule:list')")
public R<?> getPage( public R<?> getPage(
@RequestParam(value = "patientName", required = false) String patientName, @RequestParam(value = "patientName", required = false) String patientName,
@RequestParam(value = "encounterId", required = false) Long encounterId, @RequestParam(value = "encounterId", required = false) Long encounterId,
@@ -40,6 +49,8 @@ public class SurgerySafetyCheckController {
} }
@GetMapping("/list") @GetMapping("/list")
@Operation(summary = "查询安全核查列表")
@PreAuthorize("@ss.hasPermi('surgery:schedule:list')")
public R<?> getList(@RequestParam("encounterId") Long encounterId) { public R<?> getList(@RequestParam("encounterId") Long encounterId) {
LambdaQueryWrapper<SurgerySafetyCheck> w = new LambdaQueryWrapper<>(); LambdaQueryWrapper<SurgerySafetyCheck> w = new LambdaQueryWrapper<>();
w.eq(SurgerySafetyCheck::getEncounterId, encounterId) w.eq(SurgerySafetyCheck::getEncounterId, encounterId)
@@ -48,11 +59,15 @@ public class SurgerySafetyCheckController {
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@Operation(summary = "获取安全核查详情")
@PreAuthorize("@ss.hasPermi('surgery:schedule:list')")
public R<?> getById(@PathVariable Long id) { public R<?> getById(@PathVariable Long id) {
return R.ok(safetyCheckService.getById(id)); return R.ok(safetyCheckService.getById(id));
} }
@PostMapping("/add") @PostMapping("/add")
@Operation(summary = "新增安全核查")
@PreAuthorize("@ss.hasPermi('surgery:schedule:edit')")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public R<?> add(@RequestBody SurgerySafetyCheck check) { public R<?> add(@RequestBody SurgerySafetyCheck check) {
check.setCreateTime(new Date()); check.setCreateTime(new Date());
@@ -61,6 +76,8 @@ public class SurgerySafetyCheckController {
} }
@PutMapping("/update") @PutMapping("/update")
@Operation(summary = "更新安全核查")
@PreAuthorize("@ss.hasPermi('surgery:schedule:edit')")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public R<?> update(@RequestBody SurgerySafetyCheck check) { public R<?> update(@RequestBody SurgerySafetyCheck check) {
safetyCheckService.updateById(check); safetyCheckService.updateById(check);
@@ -68,9 +85,45 @@ public class SurgerySafetyCheckController {
} }
@DeleteMapping("/delete/{id}") @DeleteMapping("/delete/{id}")
@Operation(summary = "删除安全核查")
@PreAuthorize("@ss.hasPermi('surgery:schedule:remove')")
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public R<?> delete(@PathVariable Long id) { public R<?> delete(@PathVariable Long id) {
safetyCheckService.removeById(id); safetyCheckService.removeById(id);
return R.ok(); return R.ok();
} }
@GetMapping("/phase-status/{encounterId}")
@Operation(summary = "查询各阶段核查完成状态")
@PreAuthorize("@ss.hasPermi('surgery:schedule:list')")
public R<?> getPhaseStatus(@PathVariable Long encounterId) {
LambdaQueryWrapper<SurgerySafetyCheck> w = new LambdaQueryWrapper<>();
w.eq(SurgerySafetyCheck::getEncounterId, encounterId);
List<SurgerySafetyCheck> checks = safetyCheckService.list(w);
Map<String, Boolean> phaseMap = new HashMap<>();
phaseMap.put("sign_in", false);
phaseMap.put("time_out", false);
phaseMap.put("sign_out", false);
for (SurgerySafetyCheck c : checks) {
if (Boolean.TRUE.equals(c.getChecklistCompleted()) && c.getCheckPhase() != null) {
phaseMap.put(c.getCheckPhase(), true);
}
}
return R.ok(phaseMap);
}
@GetMapping("/compliance-stats")
@Operation(summary = "安全核查合规统计")
@PreAuthorize("@ss.hasPermi('surgery:schedule:list')")
public R<?> getComplianceStats() {
LambdaQueryWrapper<SurgerySafetyCheck> w = new LambdaQueryWrapper<>();
long total = safetyCheckService.count(w);
w.eq(SurgerySafetyCheck::getChecklistCompleted, true);
long completed = safetyCheckService.count(w);
Map<String, Object> stats = new HashMap<>();
stats.put("total", total);
stats.put("completed", completed);
stats.put("complianceRate", total > 0 ? (completed * 100.0 / total) : 0);
return R.ok(stats);
}
} }

View File

@@ -0,0 +1,89 @@
-- V70: 麻醉扩展功能表(术前访视、术中事件、复苏评分)
-- 麻醉前评估(术前访视)
CREATE TABLE IF NOT EXISTS anes_preop_visit (
id BIGINT PRIMARY KEY,
record_id BIGINT,
encounter_id BIGINT,
patient_id BIGINT,
patient_name VARCHAR(64),
visit_doctor_id BIGINT,
visit_doctor_name VARCHAR(64),
visit_time TIMESTAMP,
chief_complaint TEXT,
present_illness TEXT,
past_history TEXT,
allergy_history TEXT,
airway_assessment VARCHAR(255),
asa_grade VARCHAR(16),
cardiovascular_status VARCHAR(255),
respiratory_status VARCHAR(255),
neurological_status VARCHAR(255),
hepatorenal_function VARCHAR(255),
coagulation_status VARCHAR(255),
fasting_status VARCHAR(32),
npo_hours VARCHAR(16),
difficult_airway_risk VARCHAR(255),
anesthesia_risk VARCHAR(255),
risk_factors TEXT,
recommendation TEXT,
status VARCHAR(16) DEFAULT 'DRAFT',
create_by VARCHAR(64),
create_time TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
tenant_id INT DEFAULT 0,
delete_flag VARCHAR(1) DEFAULT '0'
);
-- 术中事件记录(插管/拔管/体位)
CREATE TABLE IF NOT EXISTS anes_intraop_event (
id BIGINT PRIMARY KEY,
record_id BIGINT,
encounter_id BIGINT,
patient_id BIGINT,
event_type VARCHAR(32),
event_detail TEXT,
event_time TIMESTAMP,
operator_name VARCHAR(64),
position VARCHAR(64),
intubation_type VARCHAR(64),
intubation_result VARCHAR(64),
extubation_reason VARCHAR(255),
extubation_result VARCHAR(64),
complication TEXT,
action TEXT,
remark TEXT,
create_by VARCHAR(64),
create_time TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
tenant_id INT DEFAULT 0,
delete_flag VARCHAR(1) DEFAULT '0'
);
-- 麻醉复苏评分(PACU Aldrete)
CREATE TABLE IF NOT EXISTS anes_alldrete_score (
id BIGINT PRIMARY KEY,
record_id BIGINT,
encounter_id BIGINT,
patient_id BIGINT,
patient_name VARCHAR(64),
assess_time TIMESTAMP,
activity_score INT DEFAULT 0,
respiration_score INT DEFAULT 0,
circulation_score INT DEFAULT 0,
consciousness_score INT DEFAULT 0,
spo2_score INT DEFAULT 0,
total_score INT DEFAULT 0,
risk_level VARCHAR(16),
assessor_id BIGINT,
assessor_name VARCHAR(64),
remark TEXT,
create_by VARCHAR(64),
create_time TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP,
tenant_id INT DEFAULT 0,
delete_flag VARCHAR(1) DEFAULT '0'
);