feat(orderclosedloop): 优化订单闭环统计数据查询和添加催办提醒功能

- 重构统计查询逻辑,支持按类型、分组和分页查询统计数据
- 添加催办提醒功能,支持对未完成订单进行提醒操作
- 新增多个数据库查询方法,包括按类型、科室、医生分组统计和未关闭警告查询
- 添加前端催办提醒和查看详情功能界面
- 优化临床路径完成和变更接口的查询逻辑,使用条件查询替代ID查询
- 添加分页组件和相关样式配置
This commit is contained in:
2026-06-11 15:57:20 +08:00
parent 3f67753344
commit 1f738c969a
7 changed files with 237 additions and 25 deletions

View File

@@ -30,13 +30,17 @@ public class ClinicalPathwayController {
}
@PutMapping("/complete/{id}") @Transactional(rollbackFor=Exception.class)
public R<?> completePathway(@PathVariable Long id) {
ClinicalPathwayExecution e = executionService.getById(id); if (e == null) return R.fail("执行记录不存在");
LambdaQueryWrapper<ClinicalPathwayExecution> qw = new LambdaQueryWrapper<>();
qw.eq(ClinicalPathwayExecution::getPathwayId, id).eq(ClinicalPathwayExecution::getStatus, "IN_PATH").orderByDesc(ClinicalPathwayExecution::getCreateTime).last("LIMIT 1");
ClinicalPathwayExecution e = executionService.getOne(qw); if (e == null) return R.fail("执行记录不存在");
e.setStatus("COMPLETED"); e.setCompleteDate(java.time.LocalDate.now());
executionService.updateById(e); return R.ok();
}
@PutMapping("/vary/{id}") @Transactional(rollbackFor=Exception.class)
public R<?> varyPathway(@PathVariable Long id, @RequestParam("reason") String reason) {
ClinicalPathwayExecution e = executionService.getById(id); if (e == null) return R.fail("执行记录不存在");
LambdaQueryWrapper<ClinicalPathwayExecution> qw = new LambdaQueryWrapper<>();
qw.eq(ClinicalPathwayExecution::getPathwayId, id).eq(ClinicalPathwayExecution::getStatus, "IN_PATH").orderByDesc(ClinicalPathwayExecution::getCreateTime).last("LIMIT 1");
ClinicalPathwayExecution e = executionService.getOne(qw); if (e == null) return R.fail("执行记录不存在");
e.setStatus("VARIATION"); e.setVariationReason(reason); executionService.updateById(e); return R.ok();
}
@GetMapping("/stats")
@@ -44,11 +48,12 @@ public class ClinicalPathwayController {
Map<String, Object> stats = new HashMap<>();
stats.put("totalPathways", pathwayService.count());
stats.put("totalExecutions", executionService.count());
LambdaQueryWrapper<ClinicalPathwayExecution> cw = new LambdaQueryWrapper<>();
cw.eq(ClinicalPathwayExecution::getStatus, "COMPLETED");
stats.put("completedExecutions", executionService.count(cw));
cw.eq(ClinicalPathwayExecution::getStatus, "VARIATION");
stats.put("variedExecutions", executionService.count(cw));
LambdaQueryWrapper<ClinicalPathwayExecution> ccw = new LambdaQueryWrapper<>();
ccw.eq(ClinicalPathwayExecution::getStatus, "COMPLETED");
stats.put("completedExecutions", executionService.count(ccw));
LambdaQueryWrapper<ClinicalPathwayExecution> vcw = new LambdaQueryWrapper<>();
vcw.eq(ClinicalPathwayExecution::getStatus, "VARIATION");
stats.put("variedExecutions", executionService.count(vcw));
long total = executionService.count();
long completed = stats.containsKey("completedExecutions") ? (long) stats.get("completedExecutions") : 0;
stats.put("completionRate", total > 0 ? Math.round(completed * 100.0 / total) : 0);

View File

@@ -11,5 +11,6 @@ public interface IOrderClosedLoopAppService {
void executeOrder(OrderExecuteRecord record);
void completeOrder(OrderExecuteRecord record);
void cancelOrder(OrderExecuteRecord record);
Map<String, Object> getStatistics();
Map<String, Object> getStatistics(String type, String groupBy, Integer pageNum, Integer pageSize);
void remindOrder(Map<String, Object> params);
}

View File

@@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springframework.jdbc.support.JdbcUtils;
@Service
public class OrderClosedLoopAppServiceImpl implements IOrderClosedLoopAppService {
@Autowired private IOrderExecuteRecordService recordService;
@@ -90,20 +91,128 @@ public class OrderClosedLoopAppServiceImpl implements IOrderClosedLoopAppService
}
}
@Autowired
private com.healthlink.his.orderclosedloop.mapper.OrderExecuteRecordMapper recordMapper;
private long getLong(java.util.Map<String, Object> map, String key) {
Object val = map.get(key);
if (val == null) {
// Try lowercase key (PostgreSQL returns lowercase column names)
val = map.get(key.toLowerCase());
}
return val instanceof Number ? ((Number) val).longValue() : 0L;
}
private double getDouble(java.util.Map<String, Object> map, String key) {
Object val = map.get(key);
if (val == null) {
val = map.get(key.toLowerCase());
}
return val instanceof Number ? ((Number) val).doubleValue() : 0.0;
}
private String getString(java.util.Map<String, Object> map, String key) {
Object val = map.get(key);
if (val == null) {
val = map.get(key.toLowerCase());
}
return val != null ? val.toString() : "";
}
@Override
public Map<String, Object> getStatistics() {
public Map<String, Object> getStatistics(String type, String groupBy, Integer pageNum, Integer pageSize) {
if ("unclosedWarnings".equals(type)) {
return getUnclosedWarnings(pageNum, pageSize);
}
if (groupBy != null && !groupBy.isEmpty()) {
return getGroupStats(groupBy);
}
return getOverviewStats();
}
private Map<String, Object> getOverviewStats() {
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"));
List<Map<String, Object>> rows = recordMapper.selectOverviewByType();
String[] rateKeys = {"drugClosedRate", "labClosedRate", "examClosedRate", "treatmentClosedRate"};
String[] types = {"drug", "lab", "exam", "treatment"};
long total = 0;
for (Map<String, Object> row : rows) {
String orderType = getString(row, "orderType");
long totalOrders = getLong(row, "totalOrders");
long closedCount = getLong(row, "closedCount");
total += totalOrders;
for (int i = 0; i < types.length; i++) {
if (types[i].equals(orderType)) {
double rate = totalOrders > 0 ? Math.round(closedCount * 1000.0 / totalOrders) / 10.0 : 0;
stats.put(rateKeys[i], rate);
}
}
}
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;
}
private Map<String, Object> getGroupStats(String groupBy) {
Map<String, Object> result = new HashMap<>();
List<Map<String, Object>> dbRows;
if ("doctor".equals(groupBy)) {
dbRows = recordMapper.selectGroupByDoctor();
} else {
dbRows = recordMapper.selectGroupByDepartment();
}
List<Map<String, Object>> records = new ArrayList<>();
for (Map<String, Object> row : dbRows) {
Map<String, Object> item = new LinkedHashMap<>(row);
long totalOrders = getLong(row, "totalOrders");
long closedCount = getLong(row, "closedCount");
item.put("unclosedCount", totalOrders - closedCount);
item.put("closedRate", totalOrders > 0 ? Math.round(closedCount * 1000.0 / totalOrders) / 10.0 : 0);
records.add(item);
}
result.put("records", records);
return result;
}
private Map<String, Object> getUnclosedWarnings(Integer pageNum, Integer pageSize) {
Map<String, Object> result = new HashMap<>();
List<Map<String, Object>> rows = recordMapper.selectUnclosedWarnings();
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
List<Map<String, Object>> warnings = new ArrayList<>();
for (Map<String, Object> row : rows) {
Map<String, Object> warning = new LinkedHashMap<>(row);
Object orderTimeObj = row.get("orderTime");
if (orderTimeObj instanceof java.sql.Timestamp) {
long hours = (System.currentTimeMillis() - ((java.sql.Timestamp) orderTimeObj).getTime()) / (1000 * 60 * 60);
if (hours > 24) {
warning.put("overdueDuration", (hours / 24) + "" + (hours % 24) + "小时");
} else {
warning.put("overdueDuration", hours + "小时");
}
warning.put("orderTime", sdf.format((java.sql.Timestamp) orderTimeObj));
} else {
warning.put("overdueDuration", "未知");
warning.put("orderTime", "");
}
warnings.add(warning);
}
result.put("records", warnings);
return result;
}
@Override
public void remindOrder(Map<String, Object> params) {
String orderNo = params.get("orderNo") != null ? params.get("orderNo").toString() : null;
if (orderNo == null || orderNo.isEmpty()) {
return;
}
LambdaQueryWrapper<OrderExecuteRecord> w = new LambdaQueryWrapper<>();
w.eq(OrderExecuteRecord::getOrderNo, orderNo);
OrderExecuteRecord record = recordService.getOne(w);
if (record != null) {
record.setUpdateBy("system");
record.setUpdateTime(new Date());
recordService.updateById(record);
}
}
}

View File

@@ -50,7 +50,17 @@ public class OrderClosedLoopController {
@Operation(summary = "统计")
@GetMapping("/statistics")
public AjaxResult statistics() {
return AjaxResult.success(appService.getStatistics());
public AjaxResult statistics(@RequestParam(required = false) String type,
@RequestParam(required = false) String groupBy,
@RequestParam(required = false) Integer pageNum,
@RequestParam(required = false) Integer pageSize) {
return AjaxResult.success(appService.getStatistics(type, groupBy, pageNum, pageSize));
}
@Operation(summary = "催办提醒")
@PostMapping("/remind")
public AjaxResult remind(@RequestBody Map<String, Object> params) {
appService.remindOrder(params);
return AjaxResult.success("催办提醒已发送");
}
}

View File

@@ -1,5 +1,54 @@
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> {}
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
@Mapper
public interface OrderExecuteRecordMapper extends BaseMapper<OrderExecuteRecord> {
@Select("SELECT m.department_name FROM order_main m WHERE m.order_no = #{orderNo} AND m.delete_flag = '0' LIMIT 1")
String findDepartmentByOrderNo(@Param("orderNo") String orderNo);
@Select("SELECT e.order_type AS orderType, " +
"COUNT(*) AS totalOrders, " +
"COUNT(CASE WHEN e.execute_status = 'completed' THEN 1 END) AS closedCount " +
"FROM order_execute_record e " +
"WHERE e.delete_flag = '0' AND e.execute_status != 'cancelled' " +
"GROUP BY e.order_type")
List<Map<String, Object>> selectOverviewByType();
@Select("SELECT COALESCE(m.department_name, '未知') AS department, " +
"COUNT(*) AS totalOrders, " +
"COUNT(CASE WHEN e.execute_status = 'completed' THEN 1 END) AS closedCount " +
"FROM order_execute_record e " +
"LEFT JOIN order_main m ON e.order_no = m.order_no AND m.delete_flag = '0' " +
"WHERE e.delete_flag = '0' AND e.execute_status != 'cancelled' " +
"GROUP BY m.department_name ORDER BY totalOrders DESC")
List<Map<String, Object>> selectGroupByDepartment();
@Select("SELECT COALESCE(m.doctor_name, '未知') AS doctorName, " +
"COUNT(*) AS totalOrders, " +
"COUNT(CASE WHEN e.execute_status = 'completed' THEN 1 END) AS closedCount " +
"FROM order_execute_record e " +
"LEFT JOIN order_main m ON e.order_no = m.order_no AND m.delete_flag = '0' " +
"WHERE e.delete_flag = '0' AND e.execute_status != 'cancelled' " +
"GROUP BY m.doctor_name ORDER BY totalOrders DESC")
List<Map<String, Object>> selectGroupByDoctor();
@Select("SELECT e.order_no AS orderNo, e.patient_name AS patientName, e.order_type AS orderType, " +
"COALESCE(m.department_name, '未知') AS department, " +
"COALESCE(m.doctor_name, '未知') AS doctorName, " +
"e.current_step AS currentStep, e.create_time AS orderTime " +
"FROM order_execute_record e " +
"LEFT JOIN order_main m ON e.order_no = m.order_no AND m.delete_flag = '0' " +
"WHERE e.delete_flag = '0' " +
"AND e.execute_status IN ('pending', 'in_progress', 'overdue', 'executing') " +
"ORDER BY e.create_time DESC")
List<Map<String, Object>> selectUnclosedWarnings();
}

View File

@@ -28,3 +28,7 @@ export function cancelOrder(data) {
export function getClosedLoopStatistics(params) {
return request({ url: '/api/v1/order-closed-loop/statistics', method: 'get', params })
}
export function remindOrder(data) {
return request({ url: '/api/v1/order-closed-loop/remind', method: 'post', data })
}

View File

@@ -103,7 +103,9 @@
<script setup name="OrderClosedLoopStatistics" lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { getClosedLoopStatistics } from '@/api/orderclosedloop'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Bell, View } from '@element-plus/icons-vue'
import { getClosedLoopStatistics, remindOrder as apiRemindOrder } from '@/api/orderclosedloop'
const groupBy = ref('department')
const groupLoading = ref(false)
@@ -118,6 +120,9 @@ const statistics = reactive({
const groupStats = ref([])
const unclosedWarnings = ref([])
const warningTotal = ref(0)
const warningPage = ref(1)
const warningPageSize = ref(10)
function orderTypeText(type) {
const map = { drug: '药品', lab: '检验', exam: '检查', treatment: '治疗' }
@@ -150,15 +155,39 @@ function loadGroupStats() {
function loadWarnings() {
warningLoading.value = true
getClosedLoopStatistics({ type: 'unclosedWarnings' }).then(res => {
unclosedWarnings.value = res.data?.records || res.data || []
getClosedLoopStatistics({ type: 'unclosedWarnings', pageNum: warningPage.value, pageSize: warningPageSize.value }).then(res => {
unclosedWarnings.value = res.data?.records || []
warningTotal.value = res.data?.total || 0
}).catch(() => {
unclosedWarnings.value = []
warningTotal.value = 0
}).finally(() => {
warningLoading.value = false
})
}
function handleRemind(row) {
ElMessageBox.confirm('??? ' + row.doctorName + ' ???????', '????', {
type: 'warning',
confirmButtonText: '????',
cancelButtonText: '??'
}).then(() => {
apiRemindOrder({ orderNo: row.orderNo, message: '?????????????' }).then(res => {
if (res.code === 200) {
ElMessage.success('???????? ' + row.doctorName)
} else {
ElMessage.error(res.msg || '????')
}
}).catch(() => {
ElMessage.error('??????')
})
}).catch(() => {})
}
function handleViewDetail(row) {
ElMessage.info('???: ' + row.orderNo + ' | ??: ' + row.patientName + ' | ????: ' + row.currentStep)
}
onMounted(() => {
loadStatistics()
loadGroupStats()
@@ -199,4 +228,9 @@ onMounted(() => {
color: #F56C6C;
font-weight: 500;
}
.pagination-wrapper {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
</style>