feat: 医嘱闭环模块完整开发 - Mapper/Service/Controller + API + 测试通过

This commit is contained in:
2026-06-07 17:34:17 +08:00
parent 4dd5bfeb4f
commit 5afeece809
13 changed files with 343 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
package com.healthlink.his.web.orderclosedloop.appservice;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteRecord;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteStep;
import java.util.Map;
import java.util.List;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.IPage;
public interface IOrderClosedLoopAppService {
IPage<OrderExecuteRecord> listRecords(String orderNo, String orderType, String executeStatus, Integer pageNum, Integer pageSize);
Map<String, Object> getOrderStatus(Long orderId);
void executeOrder(OrderExecuteRecord record);
void completeOrder(OrderExecuteRecord record);
void cancelOrder(OrderExecuteRecord record);
Map<String, Object> getStatistics();
}

View File

@@ -0,0 +1,109 @@
package com.healthlink.his.web.orderclosedloop.appservice.impl;
import com.healthlink.his.orderclosedloop.domain.*;
import com.healthlink.his.orderclosedloop.service.*;
import com.healthlink.his.web.orderclosedloop.appservice.IOrderClosedLoopAppService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import com.baomidou.mybatisplus.core.metadata.IPage;
@Service
public class OrderClosedLoopAppServiceImpl implements IOrderClosedLoopAppService {
@Autowired private IOrderExecuteRecordService recordService;
@Autowired private IOrderExecuteStepService stepService;
@Override
public IPage<OrderExecuteRecord> listRecords(String orderNo, String orderType, String executeStatus, Integer pageNum, Integer pageSize) {
LambdaQueryWrapper<OrderExecuteRecord> w = new LambdaQueryWrapper<>();
if (orderNo != null && !orderNo.isEmpty()) w.like(OrderExecuteRecord::getOrderNo, orderNo);
if (orderType != null && !orderType.isEmpty()) w.eq(OrderExecuteRecord::getOrderType, orderType);
if (executeStatus != null && !executeStatus.isEmpty()) w.eq(OrderExecuteRecord::getExecuteStatus, executeStatus);
w.orderByDesc(OrderExecuteRecord::getCreateTime);
return recordService.page(new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(pageNum, pageSize), w);
}
@Override
public Map<String, Object> getOrderStatus(Long orderId) {
Map<String, Object> result = new HashMap<>();
OrderExecuteRecord record = recordService.getById(orderId);
if (record == null) {
result.put("error", "记录不存在");
return result;
}
result.put("record", record);
List<OrderExecuteStep> steps = stepService.list(new LambdaQueryWrapper<OrderExecuteStep>().eq(OrderExecuteStep::getOrderNo, record.getOrderNo()).orderByAsc(OrderExecuteStep::getStepOrder));
result.put("steps", steps);
return result;
}
@Override
public void executeOrder(OrderExecuteRecord record) {
record.setExecuteStatus("executing");
record.setExecuteTime(new Date());
record.setCurrentStep("1");
recordService.save(record);
// Create default steps
String[] stepNames = {"医师开立", "药师审核", "护士执行", "执行确认"};
for (int i = 0; i < stepNames.length; i++) {
OrderExecuteStep step = new OrderExecuteStep();
step.setOrderNo(record.getOrderNo());
step.setStepName(stepNames[i]);
step.setStepOrder(i + 1);
step.setCompleted(false);
step.setTenantId(record.getTenantId() != null ? record.getTenantId() : 0);
stepService.save(step);
}
}
@Override
public void completeOrder(OrderExecuteRecord record) {
OrderExecuteRecord existing = recordService.getById(record.getId());
if (existing != null) {
existing.setExecuteStatus("completed");
existing.setCurrentStep("4");
existing.setExecutorId(record.getExecutorId());
existing.setExecutorName(record.getExecutorName());
existing.setExecuteTime(new Date());
recordService.updateById(existing);
// Complete all steps
List<OrderExecuteStep> steps = stepService.list(new LambdaQueryWrapper<OrderExecuteStep>().eq(OrderExecuteStep::getOrderNo, existing.getOrderNo()));
for (OrderExecuteStep step : steps) {
step.setCompleted(true);
step.setExecuteTime(new Date());
step.setExecutorId(record.getExecutorId());
step.setExecutorName(record.getExecutorName());
}
stepService.updateBatchById(steps);
}
}
@Override
public void cancelOrder(OrderExecuteRecord record) {
OrderExecuteRecord existing = recordService.getById(record.getId());
if (existing != null) {
existing.setExecuteStatus("cancelled");
existing.setCancelReason(record.getCancelReason());
existing.setExecutorId(record.getExecutorId());
existing.setExecutorName(record.getExecutorName());
existing.setExecuteTime(new Date());
recordService.updateById(existing);
}
}
@Override
public Map<String, Object> getStatistics() {
Map<String, Object> stats = new HashMap<>();
long total = recordService.count();
long pending = recordService.count(new LambdaQueryWrapper<OrderExecuteRecord>().eq(OrderExecuteRecord::getExecuteStatus, "pending"));
long executing = recordService.count(new LambdaQueryWrapper<OrderExecuteRecord>().eq(OrderExecuteRecord::getExecuteStatus, "executing"));
long completed = recordService.count(new LambdaQueryWrapper<OrderExecuteRecord>().eq(OrderExecuteRecord::getExecuteStatus, "completed"));
long cancelled = recordService.count(new LambdaQueryWrapper<OrderExecuteRecord>().eq(OrderExecuteRecord::getExecuteStatus, "cancelled"));
stats.put("total", total);
stats.put("pending", pending);
stats.put("executing", executing);
stats.put("completed", completed);
stats.put("cancelled", cancelled);
stats.put("completionRate", total > 0 ? Math.round(completed * 100.0 / total) : 0);
return stats;
}
}

View File

@@ -0,0 +1,56 @@
package com.healthlink.his.web.orderclosedloop.controller;
import com.core.common.core.domain.AjaxResult;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteRecord;
import com.healthlink.his.web.orderclosedloop.appservice.IOrderClosedLoopAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Tag(name = "医嘱闭环") @RestController @RequestMapping("/api/v1/order-closed-loop")
public class OrderClosedLoopController {
@Autowired private IOrderClosedLoopAppService appService;
@Operation(summary = "医嘱执行记录列表")
@GetMapping("/list")
public AjaxResult list(@RequestParam(required = false) String orderNo,
@RequestParam(required = false) String orderType,
@RequestParam(required = false) String executeStatus,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
return AjaxResult.success(appService.listRecords(orderNo, orderType, executeStatus, pageNum, pageSize));
}
@Operation(summary = "获取医嘱执行状态")
@GetMapping("/status/{orderId}")
public AjaxResult status(@PathVariable Long orderId) {
return AjaxResult.success(appService.getOrderStatus(orderId));
}
@Operation(summary = "执行医嘱")
@PostMapping("/execute")
public AjaxResult execute(@RequestBody OrderExecuteRecord record) {
appService.executeOrder(record);
return AjaxResult.success();
}
@Operation(summary = "完成医嘱")
@PostMapping("/complete")
public AjaxResult complete(@RequestBody OrderExecuteRecord record) {
appService.completeOrder(record);
return AjaxResult.success();
}
@Operation(summary = "取消医嘱")
@PostMapping("/cancel")
public AjaxResult cancel(@RequestBody OrderExecuteRecord record) {
appService.cancelOrder(record);
return AjaxResult.success();
}
@Operation(summary = "统计")
@GetMapping("/statistics")
public AjaxResult statistics() {
return AjaxResult.success(appService.getStatistics());
}
}

View File

@@ -0,0 +1,41 @@
-- 医嘱执行记录表
CREATE TABLE IF NOT EXISTS healthlink_his.order_execute_record (
id BIGSERIAL PRIMARY KEY,
order_no VARCHAR(50) NOT NULL,
encounter_id BIGINT,
patient_id BIGINT,
patient_name VARCHAR(100),
order_type VARCHAR(20) NOT NULL,
order_content TEXT,
current_step VARCHAR(50),
execute_status VARCHAR(20) DEFAULT 'pending',
executor_id BIGINT,
executor_name VARCHAR(100),
execute_time TIMESTAMP,
cancel_reason TEXT,
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- 医嘱执行步骤表(闭环流程)
CREATE TABLE IF NOT EXISTS healthlink_his.order_execute_step (
id BIGSERIAL PRIMARY KEY,
order_no VARCHAR(50) NOT NULL,
step_name VARCHAR(100) NOT NULL,
step_order INTEGER NOT NULL,
completed BOOLEAN DEFAULT FALSE,
executor_id BIGINT,
executor_name VARCHAR(100),
execute_time TIMESTAMP,
remark TEXT,
tenant_id INTEGER DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_order_exec_order_no ON healthlink_his.order_execute_record(order_no);
CREATE INDEX IF NOT EXISTS idx_order_exec_status ON healthlink_his.order_execute_record(execute_status);
CREATE INDEX IF NOT EXISTS idx_order_step_order_no ON healthlink_his.order_execute_step(order_no);

View File

@@ -0,0 +1,32 @@
package com.healthlink.his.orderclosedloop.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
import java.util.List;
@Data
@TableName("order_execute_record")
@EqualsAndHashCode(callSuper = true)
public class OrderExecuteRecord extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private String orderNo;
private Long encounterId;
private Long patientId;
private String patientName;
private String orderType;
private String orderContent;
private String currentStep;
private String executeStatus;
private Long executorId;
private String executorName;
private Date executeTime;
private String cancelReason;
}

View File

@@ -0,0 +1,26 @@
package com.healthlink.his.orderclosedloop.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.util.Date;
@Data
@TableName("order_execute_step")
public class OrderExecuteStep {
@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private String orderNo;
private String stepName;
private Integer stepOrder;
private Boolean completed;
private Long executorId;
private String executorName;
private Date executeTime;
private String remark;
private Integer tenantId;
private Date createTime;
}

View File

@@ -0,0 +1,5 @@
package com.healthlink.his.orderclosedloop.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface OrderExecuteRecordMapper extends BaseMapper<OrderExecuteRecord> {}

View File

@@ -0,0 +1,5 @@
package com.healthlink.his.orderclosedloop.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteStep;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface OrderExecuteStepMapper extends BaseMapper<OrderExecuteStep> {}

View File

@@ -0,0 +1,4 @@
package com.healthlink.his.orderclosedloop.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteRecord;
public interface IOrderExecuteRecordService extends IService<OrderExecuteRecord> {}

View File

@@ -0,0 +1,4 @@
package com.healthlink.his.orderclosedloop.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteStep;
public interface IOrderExecuteStepService extends IService<OrderExecuteStep> {}

View File

@@ -0,0 +1,8 @@
package com.healthlink.his.orderclosedloop.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteRecord;
import com.healthlink.his.orderclosedloop.mapper.OrderExecuteRecordMapper;
import com.healthlink.his.orderclosedloop.service.IOrderExecuteRecordService;
import org.springframework.stereotype.Service;
@Service
public class OrderExecuteRecordServiceImpl extends ServiceImpl<OrderExecuteRecordMapper, OrderExecuteRecord> implements IOrderExecuteRecordService {}

View File

@@ -0,0 +1,8 @@
package com.healthlink.his.orderclosedloop.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteStep;
import com.healthlink.his.orderclosedloop.mapper.OrderExecuteStepMapper;
import com.healthlink.his.orderclosedloop.service.IOrderExecuteStepService;
import org.springframework.stereotype.Service;
@Service
public class OrderExecuteStepServiceImpl extends ServiceImpl<OrderExecuteStepMapper, OrderExecuteStep> implements IOrderExecuteStepService {}

View File

@@ -0,0 +1,30 @@
import request from '@/utils/request'
// ==================== 医嘱闭环 ====================
export function listOrderExecuteRecord(params) {
return request({ url: '/api/v1/order-closed-loop/list', method: 'get', params })
}
export function getOrderClosedLoopStatus(orderId) {
return request({ url: `/api/v1/order-closed-loop/status/${orderId}`, method: 'get' })
}
export function getOrderStatistics() {
return request({ url: '/api/v1/order-closed-loop/statistics', method: 'get' })
}
export function executeOrder(data) {
return request({ url: '/api/v1/order-closed-loop/execute', method: 'post', data })
}
export function completeOrder(data) {
return request({ url: '/api/v1/order-closed-loop/complete', method: 'post', data })
}
export function cancelOrder(data) {
return request({ url: '/api/v1/order-closed-loop/cancel', method: 'post', data })
}
export function getClosedLoopStatistics(params) {
return request({ url: '/api/v1/order-closed-loop/statistics', method: 'get', params })
}