Index: ssts-reports/src/test/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/handler/UseRecordRecyclingDelayHandlerLoadUseRecordRecyclingDelayListTest.java =================================================================== diff -u --- ssts-reports/src/test/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/handler/UseRecordRecyclingDelayHandlerLoadUseRecordRecyclingDelayListTest.java (revision 0) +++ ssts-reports/src/test/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/handler/UseRecordRecyclingDelayHandlerLoadUseRecordRecyclingDelayListTest.java (revision 41245) @@ -0,0 +1,172 @@ +package com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.handler; + +import com.forgon.databaseadapter.service.DateQueryAdapter; +import com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.model.TousseInstanceInfo; +import com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.model.UseRecordRecyclingDelayReportVo; +import com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.param.UseRecordRecyclingDelayParam; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * UseRecordRecyclingDelayHandler 中 loadUseRecordRecyclingDelayList 方法的单元测试 + */ +public class UseRecordRecyclingDelayHandlerLoadUseRecordRecyclingDelayListTest { + + @InjectMocks + @Spy + private UseRecordRecyclingDelayHandlerImpl handler; + + @Mock + private DateQueryAdapter dateQueryAdapter; + + @Mock + private JdbcTemplate jdbcTemplate; + + private AutoCloseable closeable; + + @Before + public void setUp() { + closeable = MockitoAnnotations.openMocks(this); + } + + @After + public void tearDown() throws Exception { + if (closeable != null) { + closeable.close(); + } + } + + /** + * 测试正常情况:输入有效参数,返回正确结构化数据 + */ + @Test + public void testLoadUseRecordRecyclingDelayList_NormalCase_ReturnCorrectData() throws ParseException { + // 准备参数 + UseRecordRecyclingDelayParam param = new UseRecordRecyclingDelayParam(); + param.setStartDay("2023-01-01 00:00:00"); + param.setEndDay("2023-01-31 23:59:59"); + param.setOperation("手术一"); + param.setDoctorName("张医生"); + param.setTousseName("器械包A"); + param.setDepartCode("A01"); + + // 构造mock数据 + List mockInstances = new ArrayList<>(); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + TousseInstanceInfo info1 = new TousseInstanceInfo(); + info1.setEnteringDate(sdf.parse("2023-01-01 10:00:00")); + info1.setStatus("已回收"); + info1.setRecyclingTime(sdf.parse("2023-01-01 10:25:00")); // delayType = 0 + + TousseInstanceInfo info2 = new TousseInstanceInfo(); + info2.setEnteringDate(sdf.parse("2023-01-02 11:00:00")); + info2.setStatus("已回收"); + info2.setRecyclingTime(sdf.parse("2023-01-02 11:45:00")); // delayType = 1 + + mockInstances.add(info1); + mockInstances.add(info2); + + doReturn(mockInstances).when(handler).findTousseInstances(param.getStartDay(), param.getEndDay(), + param.getOperation(), param.getDoctorName(), param.getTousseName(), param.getDepartCode()); + // Mock依赖的方法 + when(jdbcTemplate.query(anyString(), any(BeanPropertyRowMapper.class))).thenReturn(mockInstances); + + when(dateQueryAdapter.dateConverAdapter2(anyString(), anyString())).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + return "'" + args[0] + "'"; + }); + + when(dateQueryAdapter.datetimeDiffGreaterOrEqual(anyString(), anyString(), anyInt())) + .thenReturn("TIMESTAMPDIFF(MINUTE, ur.enteringDate, rr.recyclingTime) >= 30"); + + // 执行方法 + List result = handler.loadUseRecordRecyclingDelayList(param); + + // 断言验证 + assertNotNull(result); + assertEquals(2, result.size()); + + UseRecordRecyclingDelayReportVo firstVo = result.get(0); + assertEquals("2023-01-01", firstVo.getDate()); + assertEquals(Integer.valueOf(0), firstVo.getHalfHourRecycledCount()); + assertEquals(1, firstVo.getTousseInstances().size()); + + UseRecordRecyclingDelayReportVo secondVo = result.get(1); + assertEquals("2023-01-02", secondVo.getDate()); + assertEquals(Integer.valueOf(1), secondVo.getHalfHourRecycledCount()); + assertEquals(1, secondVo.getTousseInstances().size()); + } + + /** + * 测试边界条件:没有满足条件的数据时返回空列表 + */ + @Test + public void testLoadUseRecordRecyclingDelayList_NoMatchingData_ReturnEmptyList() { + UseRecordRecyclingDelayParam param = new UseRecordRecyclingDelayParam(); + param.setStartDay("2023-01-01 00:00:00"); + param.setEndDay("2023-01-31 23:59:59"); + + when(jdbcTemplate.query(anyString(), any(BeanPropertyRowMapper.class))).thenReturn(Collections.emptyList()); + + when(dateQueryAdapter.dateConverAdapter2(anyString(), anyString())).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + return "'" + args[0] + "'"; + }); + + when(dateQueryAdapter.datetimeDiffGreaterOrEqual(anyString(), anyString(), anyInt())) + .thenReturn("TIMESTAMPDIFF(MINUTE, ur.enteringDate, rr.recyclingTime) >= 30"); + + List result = handler.loadUseRecordRecyclingDelayList(param); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + /** + * 测试非法参数:起始时间晚于结束时间,应该抛出RuntimeException + */ + @Test + public void testLoadUseRecordRecyclingDelayList_InvalidDateRange_ThrowsException() { + UseRecordRecyclingDelayParam param = new UseRecordRecyclingDelayParam(); + param.setStartDay("2023-02-01 00:00:00"); + param.setEndDay("2023-01-01 00:00:00"); + + assertThrows(RuntimeException.class, () -> handler.loadUseRecordRecyclingDelayList(param)); + } + + /** + * 测试非法参数:缺少开始或结束时间,应该抛出RuntimeException + */ + @Test + public void testLoadUseRecordRecyclingDelayList_MissingDate_ThrowsException() { + UseRecordRecyclingDelayParam param = new UseRecordRecyclingDelayParam(); + param.setStartDay(null); + param.setEndDay("2023-01-01 00:00:00"); + + assertThrows(RuntimeException.class, () -> handler.loadUseRecordRecyclingDelayList(param)); + + param.setStartDay("2023-01-01 00:00:00"); + param.setEndDay(null); + + assertThrows(RuntimeException.class, () -> handler.loadUseRecordRecyclingDelayList(param)); + } +} Index: ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/model/RecyclingSummary.java =================================================================== diff -u --- ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/model/RecyclingSummary.java (revision 0) +++ ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/model/RecyclingSummary.java (revision 41245) @@ -0,0 +1,69 @@ +package com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.model; + +import java.util.Objects; + +public class RecyclingSummary { + /** + * 大于或等于半小时且小于1小时回收器械包数量 + */ + protected Integer halfHourRecycledCount = 0; + /** + * 大于或等于1小时且小于2小时回收器械包数量 + */ + protected Integer oneHourRecycledCount = 0; + /** + * 大于或等于2小时回收器械包数量 + */ + protected Integer twoHourRecycledCount = 0; + + public Integer getHalfHourRecycledCount() { + return halfHourRecycledCount; + } + + public void setHalfHourRecycledCount(Integer halfHourRecycledCount) { + this.halfHourRecycledCount = halfHourRecycledCount; + } + + public Integer getOneHourRecycledCount() { + return oneHourRecycledCount; + } + + public void setOneHourRecycledCount(Integer oneHourRecycledCount) { + this.oneHourRecycledCount = oneHourRecycledCount; + } + + public Integer getTwoHourRecycledCount() { + return twoHourRecycledCount; + } + + public void setTwoHourRecycledCount(Integer twoHourRecycledCount) { + this.twoHourRecycledCount = twoHourRecycledCount; + } + + public void addHalfHourRecycledCount(Integer halfHourRecycledCount) { + this.halfHourRecycledCount += halfHourRecycledCount; + } + + public void addOneHourRecycledCount(Integer oneHourRecycledCount) { + this.oneHourRecycledCount += oneHourRecycledCount; + } + + public void addTwoHourRecycledCount(Integer twoHourRecycledCount) { + this.twoHourRecycledCount += twoHourRecycledCount; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof RecyclingSummary)) return false; + RecyclingSummary that = (RecyclingSummary) object; + return Objects.equals(halfHourRecycledCount, that.halfHourRecycledCount) + && Objects.equals(oneHourRecycledCount, that.oneHourRecycledCount) + && Objects.equals(twoHourRecycledCount, that.twoHourRecycledCount); + } + + @Override + public int hashCode() { + return Objects.hash(halfHourRecycledCount, oneHourRecycledCount, twoHourRecycledCount); + } +} Index: ssts-reports/src/test/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/handler/UseRecordRecyclingDelayHandlerFindTousseInstancesTest.java =================================================================== diff -u --- ssts-reports/src/test/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/handler/UseRecordRecyclingDelayHandlerFindTousseInstancesTest.java (revision 0) +++ ssts-reports/src/test/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/handler/UseRecordRecyclingDelayHandlerFindTousseInstancesTest.java (revision 41245) @@ -0,0 +1,226 @@ +package com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.handler; + +import com.forgon.databaseadapter.service.DateQueryAdapter; +import com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.model.TousseInstanceInfo; +import com.forgon.tools.util.ForgonDateUtils; +import com.forgon.tools.util.SqlUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.*; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * UseRecordRecyclingDelayHandler 单元测试类 + */ +public class UseRecordRecyclingDelayHandlerFindTousseInstancesTest { + + @InjectMocks + private UseRecordRecyclingDelayHandlerImpl handler; + + @Mock + private DateQueryAdapter dateQueryAdapter; + + @Mock + private JdbcTemplate jdbcTemplate; + private MockedStatic sqlUtilsMock; + private AutoCloseable autoCloseable; + + @Before + public void setUp() { + autoCloseable = MockitoAnnotations.openMocks(this); + sqlUtilsMock = Mockito.mockStatic(SqlUtils.class); + } + + @After + public void tearDown() throws Exception { + // 清理静态mock + autoCloseable.close(); + sqlUtilsMock.close(); + } + + /** + * 测试正常流程 + */ + @Test + public void testFindTousseInstances_Normal() { + // 准备数据 + String fmt = "yyyy-MM-dd HH24:MI:SS"; + String startDay = "2023-01-01 00:00:00"; + String endDay = "2023-01-31 23:59:59"; + String operation = "手术1"; + String doctorName = "医生1"; + String tousseName = "包1"; + String departCode = "01"; + + // Mock DateQueryAdapter + when(dateQueryAdapter.dateConverAdapter2("2023-01-01 00:00:00", fmt)).thenReturn("TO_DATE('2023-01-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS')"); + when(dateQueryAdapter.dateConverAdapter2("2023-01-31 23:59:59", fmt)).thenReturn("TO_DATE('2023-01-31 23:59:59', 'YYYY-MM-DD HH24:MI:SS')"); + when(dateQueryAdapter.datetimeDiffGreaterOrEqual(anyString(), anyString(), anyInt())).thenReturn("datetime_diff_condition"); + + // Mock JdbcTemplate + List expectedList = new ArrayList<>(); + TousseInstanceInfo info = new TousseInstanceInfo(); + info.setTousseName("包1"); + expectedList.add(info); + when(jdbcTemplate.query(anyString(), any(BeanPropertyRowMapper.class))).thenReturn(expectedList); + + // 执行测试 + List result = handler.findTousseInstances(startDay, endDay, operation, doctorName, tousseName, departCode); + + // 验证结果 + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("包1", result.get(0).getTousseName()); + + // 验证SQL是否包含所有条件 + ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); + verify(jdbcTemplate).query(sqlCaptor.capture(), any(BeanPropertyRowMapper.class)); + String sql = sqlCaptor.getValue(); + System.out.println(sql); + assertTrue(sql.contains("ur.operation = '手术1'")); + assertTrue(sql.contains("ur.doctorName like '%医生1%'")); + assertTrue(sql.contains("ti.tousseName = '包1'")); + assertTrue(sql.contains("datetime_diff_condition or rr.recyclingTime is null")); + assertTrue(sql.contains("ur.enteringDate between TO_DATE('2023-01-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS') and TO_DATE('2023-01-31 23:59:59', 'YYYY-MM-DD HH24:MI:SS')")); + assertTrue(sql.contains("ti.departCoding = '01'")); + } + + /** + * 测试正常流程 + */ + @Test + public void testFindTousseInstances_OnlyDateFilter_Normal() { + // 准备数据 + String fmt = "yyyy-MM-dd HH24:MI:SS"; + String startDay = "2023-01-01 00:00:00"; + String endDay = "2023-01-31 23:59:59"; + String operation = ""; + String doctorName = ""; + String tousseName = ""; + String departCode = ""; + + // Mock DateQueryAdapter + when(dateQueryAdapter.dateConverAdapter2("2023-01-01 00:00:00", fmt)).thenReturn("TO_DATE('2023-01-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS')"); + when(dateQueryAdapter.dateConverAdapter2("2023-01-31 23:59:59", fmt)).thenReturn("TO_DATE('2023-01-31 23:59:59', 'YYYY-MM-DD HH24:MI:SS')"); + when(dateQueryAdapter.datetimeDiffGreaterOrEqual(anyString(), anyString(), anyInt())).thenReturn("datetime_diff_condition"); + + // Mock JdbcTemplate + List expectedList = new ArrayList<>(); + TousseInstanceInfo info = new TousseInstanceInfo(); + info.setTousseName("包1"); + expectedList.add(info); + when(jdbcTemplate.query(anyString(), any(BeanPropertyRowMapper.class))).thenReturn(expectedList); + + // 执行测试 + List result = handler.findTousseInstances(startDay, endDay, operation, doctorName, tousseName, departCode); + + // 验证结果 + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("包1", result.get(0).getTousseName()); + + // 验证SQL是否包含所有条件 + ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); + verify(jdbcTemplate).query(sqlCaptor.capture(), any(BeanPropertyRowMapper.class)); + String sql = sqlCaptor.getValue(); + System.out.println(sql); + assertFalse(sql.contains("ur.operation = ''")); + assertFalse(sql.contains("ur.doctorName like '%%'")); + assertFalse(sql.contains("td.tousseName = ''")); + assertFalse(sql.contains("ti.departCoding = ''")); + assertTrue(sql.contains("datetime_diff_condition or rr.recyclingTime is null")); + assertTrue(sql.contains("ur.enteringDate between TO_DATE('2023-01-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS') and TO_DATE('2023-01-31 23:59:59', 'YYYY-MM-DD HH24:MI:SS')")); + } + + /** + * 测试 startDay 为空 + */ + @Test + public void testFindTousseInstances_StartDayEmpty() { + String startDay = ""; + String endDay = "2023-01-31 23:59:59"; + + Exception exception = assertThrows(RuntimeException.class, () -> { + handler.findTousseInstances(startDay, endDay, null, null, null, null); + }); + + assertEquals("请选择开始时间和结束时间", exception.getMessage()); + } + + /** + * 测试 endDay 为空 + */ + @Test + public void testFindTousseInstances_EndDayEmpty() { + String startDay = "2023-01-01 00:00:00"; + String endDay = ""; + + Exception exception = assertThrows(RuntimeException.class, () -> { + handler.findTousseInstances(startDay, endDay, null, null, null, null); + }); + + assertEquals("请选择开始时间和结束时间", exception.getMessage()); + } + + /** + * 测试时间范围超过31天 + */ + @Test + public void testFindTousseInstances_DateRangeExceeds31Days() { + String startDay = "2023-01-01 00:00:00"; + String endDay = "2023-02-02 00:00:00"; // 超过31天 + + try (MockedStatic mockedForgonDateUtils = Mockito.mockStatic(ForgonDateUtils.class)) { + mockedForgonDateUtils.when(() -> ForgonDateUtils.safelyParseDate(anyString(), anyString())) + .thenAnswer(invocation -> { + String dateStr = invocation.getArgument(0); + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateStr); + }); + + Exception exception = assertThrows(RuntimeException.class, () -> { + handler.findTousseInstances(startDay, endDay, null, null, null, null); + }); + + assertEquals("请选择正确的开始时间和结束日期且相差不超过31天", exception.getMessage()); + } catch (Exception e) { + fail("Unexpected exception: " + e.getMessage()); + } + } + + /** + * 测试 operation 包含非法字符 + */ + @Test + public void testFindTousseInstances_InvalidOperation() { + String startDay = "2023-01-01 00:00:00"; + String endDay = "2023-01-31 23:59:59"; + String errStr = "假如有异常字符'"; // 包含非法字符 + + sqlUtilsMock.when(() -> SqlUtils.checkInputParam(errStr)).thenThrow(new RuntimeException("参数异常")); + + Exception exception = assertThrows(RuntimeException.class, () -> + handler.findTousseInstances(startDay, endDay, errStr, null, null, null)); + assertEquals("参数异常", exception.getMessage()); + + exception = assertThrows(RuntimeException.class, () -> + handler.findTousseInstances(startDay, endDay, null, errStr, null, null)); + assertEquals("参数异常", exception.getMessage()); + + exception = assertThrows(RuntimeException.class, () -> + handler.findTousseInstances(startDay, endDay, null, null, errStr, null)); + assertEquals("参数异常", exception.getMessage()); + + exception = assertThrows(RuntimeException.class, () -> + handler.findTousseInstances(startDay, endDay, null, null, null, errStr)); + assertEquals("参数异常", exception.getMessage()); + } +} Index: ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/model/UseRecordRecyclingDelayReportVo.java =================================================================== diff -u --- ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/model/UseRecordRecyclingDelayReportVo.java (revision 0) +++ ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/model/UseRecordRecyclingDelayReportVo.java (revision 41245) @@ -0,0 +1,48 @@ +package com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.model; + +import java.util.List; +import java.util.Objects; + +/** + * ZSYKEQ-12:未及时回收处理的器械包统计报表。统计转换申请单后到回收的不同时长的器械包数量 + */ +public class UseRecordRecyclingDelayReportVo extends RecyclingSummary { + /** + * 日期。格式为yyyy-MM-dd + */ + private String date; + /** + * 延时回收器械包明细列表。都是半小时以上回收或者超过半小时还未回收的器械包 + */ + private List tousseInstances; + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + public List getTousseInstances() { + return tousseInstances; + } + + public void setTousseInstances(List tousseInstances) { + this.tousseInstances = tousseInstances; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof UseRecordRecyclingDelayReportVo)) return false; + if (!super.equals(object)) return false; + UseRecordRecyclingDelayReportVo that = (UseRecordRecyclingDelayReportVo) object; + return Objects.equals(date, that.date) && Objects.equals(tousseInstances, that.tousseInstances); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), date, tousseInstances); + } +} Index: ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/model/TousseInstanceInfo.java =================================================================== diff -u --- ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/model/TousseInstanceInfo.java (revision 0) +++ ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/model/TousseInstanceInfo.java (revision 41245) @@ -0,0 +1,196 @@ +package com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.model; + +import java.util.Date; +import java.util.Objects; + +public class TousseInstanceInfo { + private Long useRecordId; + /** + * 手术间,也叫室间 + */ + private String operationRoom; + /** + * 手术名称 + */ + private String operation; + /** + * 患者名称 + */ + private String patientName; + /** + * 巡回护士 + */ + private String circuitNurse; + /** + * 器械包名称 + */ + private String tousseName; + /** + * 器械包条码 + */ + private String barcode; + /** + * 器械包状态 + */ + private String status; + /** + * 器械包回收时间,是二次回收的时间 + */ + private Date recyclingTime; + /** + * 录入使用记录的时间 + */ + private Date enteringDate; + + public TousseInstanceInfo() { + } + + public TousseInstanceInfo(Long useRecordId, String operationRoom, String operation, String patientName, + String circuitNurse, String tousseName, String barcode, String status, Date recyclingTime, Date enteringDate) { + this.useRecordId = useRecordId; + this.operationRoom = operationRoom; + this.operation = operation; + this.patientName = patientName; + this.circuitNurse = circuitNurse; + this.tousseName = tousseName; + this.barcode = barcode; + this.status = status; + this.recyclingTime = recyclingTime; + this.enteringDate = enteringDate; + } + + public Long getUseRecordId() { + return useRecordId; + } + + public void setUseRecordId(Long useRecordId) { + this.useRecordId = useRecordId; + } + + public String getOperationRoom() { + return operationRoom; + } + + public void setOperationRoom(String operationRoom) { + this.operationRoom = operationRoom; + } + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public String getPatientName() { + return patientName; + } + + public void setPatientName(String patientName) { + this.patientName = patientName; + } + + public String getCircuitNurse() { + return circuitNurse; + } + + public void setCircuitNurse(String circuitNurse) { + this.circuitNurse = circuitNurse; + } + + public String getTousseName() { + return tousseName; + } + + public void setTousseName(String tousseName) { + this.tousseName = tousseName; + } + + public String getBarcode() { + return barcode; + } + + public void setBarcode(String barcode) { + this.barcode = barcode; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Date getRecyclingTime() { + return recyclingTime; + } + + public void setRecyclingTime(Date recyclingTime) { + this.recyclingTime = recyclingTime; + } + + public Date getEnteringDate() { + return enteringDate; + } + + public void setEnteringDate(Date enteringDate) { + this.enteringDate = enteringDate; + } + + public int getRecyclingDelayType() { + if(enteringDate == null){ + return -1; // 出错了 + } + Date end = recyclingTime == null ? new Date() : recyclingTime; + long diff = end.getTime() - enteringDate.getTime(); + if(diff < 30*60*1000){ + //半小时内回收,是正常的,没有延时 + return 0; + }else if(diff < 60 * 60 * 1000){ + //半小时到1小时内回收,算半小时的范围 + return 1; + }else if(diff < 2*60*60*1000){ + //1小时到2小时内回收,算1小时的范围 + return 2; + }else{ + //超过2小时回收,算2小时范围 + return 3; + } + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof TousseInstanceInfo)) return false; + TousseInstanceInfo that = (TousseInstanceInfo) object; + return Objects.equals(useRecordId, that.useRecordId) && Objects.equals(operationRoom, that.operationRoom) + && Objects.equals(operation, that.operation) && Objects.equals(patientName, that.patientName) + && Objects.equals(circuitNurse, that.circuitNurse) && Objects.equals(tousseName, that.tousseName) + && Objects.equals(barcode, that.barcode) && Objects.equals(status, that.status) + && Objects.equals(recyclingTime, that.recyclingTime) && Objects.equals(enteringDate, that.enteringDate); + } + + @Override + public int hashCode() { + return Objects.hash(useRecordId, operationRoom, operation, patientName, circuitNurse, tousseName, barcode, + status, recyclingTime, enteringDate); + } + + @Override + public String toString() { + return "TousseInstanceInfo{" + + "useRecordId=" + useRecordId + + ", operationRoom='" + operationRoom + '\'' + + ", operation='" + operation + '\'' + + ", patientName='" + patientName + '\'' + + ", circuitNurse='" + circuitNurse + '\'' + + ", tousseName='" + tousseName + '\'' + + ", barcode='" + barcode + '\'' + + ", status='" + status + '\'' + + ", recyclingTime=" + recyclingTime + + ", enteringDate=" + enteringDate + + '}'; + } +} Index: ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/handler/UseRecordRecyclingDelayHandlerImpl.java =================================================================== diff -u --- ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/handler/UseRecordRecyclingDelayHandlerImpl.java (revision 0) +++ ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/handler/UseRecordRecyclingDelayHandlerImpl.java (revision 41245) @@ -0,0 +1,119 @@ +package com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.handler; + +import com.forgon.databaseadapter.service.DateQueryAdapter; +import com.forgon.directory.model.BarcodeDevice; +import com.forgon.disinfectsystem.entity.basedatamanager.toussedefinition.TousseDefinition; +import com.forgon.disinfectsystem.entity.basedatamanager.toussedefinition.TousseInstance; +import com.forgon.disinfectsystem.entity.recyclingrecord.RecyclingItem; +import com.forgon.disinfectsystem.entity.recyclingrecord.RecyclingRecord; +import com.forgon.disinfectsystem.entity.useRecord.UseRecord; +import com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.model.TousseInstanceInfo; +import com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.model.UseRecordRecyclingDelayReportVo; +import com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.param.UseRecordRecyclingDelayParam; +import com.forgon.tools.util.ForgonDateUtils; +import com.forgon.tools.util.SqlUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.*; + +@Service +public class UseRecordRecyclingDelayHandlerImpl implements UseRecordRecyclingDelayHandler { + @Autowired + private DateQueryAdapter dateQueryAdapter; + @Autowired + private JdbcTemplate jdbcTemplate; + + @Override + public List loadUseRecordRecyclingDelayList(UseRecordRecyclingDelayParam param) { + // 查找器械包实例 + List tousseInstanceInfoList = findTousseInstances(param.getStartDay(), param.getEndDay(), + param.getOperation(), param.getDoctorName(), param.getTousseName(), param.getDepartCode()); + // 分类 + Map reportVoMap = classify(tousseInstanceInfoList); + + List vos = new ArrayList<>(reportVoMap.values()); + vos.sort(Comparator.comparing(UseRecordRecyclingDelayReportVo::getDate)); + vos.forEach(vo -> vo.getTousseInstances().sort(Comparator.comparing(TousseInstanceInfo::getTousseName))); + return vos; + } + + Map classify(List tousseInstanceInfoList) { + Map reportVoMap = new HashMap<>(); + tousseInstanceInfoList.forEach(item -> { + int delayType = item.getRecyclingDelayType(); + String date = ForgonDateUtils.safelyFormatDate(item.getEnteringDate(), ForgonDateUtils.DATE_FORMAT_YYYYMMDD, ""); + UseRecordRecyclingDelayReportVo vo = reportVoMap.get(date); + if(vo == null){ + vo = new UseRecordRecyclingDelayReportVo(); + vo.setDate(date); + reportVoMap.put(date, vo); + } + if(delayType == 1){ + vo.addHalfHourRecycledCount(1); + }else if(delayType == 2){ + vo.addOneHourRecycledCount(1); + }else if(delayType == 3){ + vo.addTwoHourRecycledCount(1); + } + List list = vo.getTousseInstances(); + if(list == null){ + list = new ArrayList<>(); + vo.setTousseInstances(list); + } + list.add(item); + }); + return reportVoMap; + } + + List findTousseInstances(String startDay, String endDay, String operation, String doctorName, String tousseName, String departCode) { + String fmt = "yyyy-MM-dd HH24:MI:SS"; + if(StringUtils.isEmpty(startDay) || StringUtils.isEmpty(endDay)){ + throw new RuntimeException("请选择开始时间和结束时间"); + } + Date start = ForgonDateUtils.safelyParseDate(startDay, ForgonDateUtils.DATE_FORMAT_YYYYMMDDHHMMSS); + Date end = ForgonDateUtils.safelyParseDate(endDay, ForgonDateUtils.DATE_FORMAT_YYYYMMDDHHMMSS); + validateParam(start, end, operation, doctorName, tousseName, departCode); + Date current30MinutesAgo = new Date(System.currentTimeMillis() - 30 * 60 * 1000); + String startStr = dateQueryAdapter.dateConverAdapter2(ForgonDateUtils.safelyFormatDate(start, ForgonDateUtils.DATE_FORMAT_YYYYMMDDHHMMSS,""), fmt); + String endStr = dateQueryAdapter.dateConverAdapter2(ForgonDateUtils.safelyFormatDate(end, ForgonDateUtils.DATE_FORMAT_YYYYMMDDHHMMSS, ""), fmt); + String cur30MinAgoStr = dateQueryAdapter.dateConverAdapter2(ForgonDateUtils.safelyFormatDate(current30MinutesAgo, ForgonDateUtils.DATE_FORMAT_YYYYMMDDHHMMSS, ""), fmt); + + + String sql = "select ti.tousseName, ti.status, ti.useRecord_id useRecordId, bd.barcode, ur.operationRoom, ur.operation, ur.patientName, ur.circuitNurse, ur.enteringDate, rr.recyclingTime from " + + TousseInstance.class.getSimpleName()+" ti join "+TousseDefinition.class.getSimpleName()+" td on td.id = ti.tousseDefinition_id join " + + BarcodeDevice.class.getSimpleName()+" bd on bd.id = ti.id left join "+ RecyclingItem.class.getSimpleName()+" ri on ri.id = ti.recyclingItemId left join " + + RecyclingRecord.class.getSimpleName()+" rr on rr.id = ri.recyclingRecord_id join " + + UseRecord.class.getSimpleName()+" ur on ur.id=ti.useRecord_id where td.tousseType <> '敷料包' and td.isRecycling='是' " + + "and ur.enteringDate between " + startStr + " and " + endStr + + " and ur.enteringDate <= " + cur30MinAgoStr + + " and (" + dateQueryAdapter.datetimeDiffGreaterOrEqual("ur.enteringDate", "rr.recyclingTime", 30) + " or rr.recyclingTime is null) "; + if(StringUtils.hasText(operation)) { + sql += " and ur.operation = '" + operation.trim() + "'"; + } + if(StringUtils.hasText(doctorName)) { + sql += " and ur.doctorName like '%" + doctorName.trim() + "%'"; + } + if(StringUtils.hasText(tousseName)) { + sql += " and ti.tousseName = '" + tousseName.trim() + "'"; + } + if(StringUtils.hasText(departCode)) { + sql += " and ti.departCoding = '" + departCode.trim() + "'"; + } + return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(TousseInstanceInfo.class)); + } + + private void validateParam(Date start, Date end, String operation, String doctorName, String tousseName, String departCode) { + // 开始时间和结束时间相差不能大于31天且开始时间在结束时间之前 + if(start == null || end == null || start.after(end) || end.getTime() - start.getTime() > 31L * 24 * 60 * 60 * 1000) { + throw new RuntimeException("请选择正确的开始时间和结束日期且相差不超过31天"); + } + SqlUtils.checkInputParam(operation); + SqlUtils.checkInputParam(doctorName); + SqlUtils.checkInputParam(tousseName); + SqlUtils.checkInputParam(departCode); + } +} Index: ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/handler/UseRecordRecyclingDelayHandler.java =================================================================== diff -u --- ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/handler/UseRecordRecyclingDelayHandler.java (revision 0) +++ ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/handler/UseRecordRecyclingDelayHandler.java (revision 41245) @@ -0,0 +1,22 @@ +package com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.handler; + +import com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.model.UseRecordRecyclingDelayReportVo; +import com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.param.UseRecordRecyclingDelayParam; + +import java.util.List; + +/** + * ZSYKEQ-12 未及时回收处理的器械包统计报表 + * 非通用功能,项目现场是使用记录在录入的时候就审核并转换生成申请单。所以使用时间与申请单的时间是同一个时间。理论上计划回收时间差应该使用申请单时间与回收记录的时间计算差值。 + * 但是此项目使用时间与申请时间相同,而器械包实例与二次回收记录以及使用记录关联且直接记录了使用时间,而回收项与回收记录关联。所以以器械包实例为中心,可以直接查出相关数据且关联关系是明确的。 + * 所以这里的实现以器械包实例为中心进行 + */ +public interface UseRecordRecyclingDelayHandler { + + /** + * 查询未及时回收器械包列表 + * @param param 查询参数 + * @return 未及时回收器械包列表 + */ + List loadUseRecordRecyclingDelayList(UseRecordRecyclingDelayParam param); +} Index: ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/param/UseRecordRecyclingDelayParam.java =================================================================== diff -u --- ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/param/UseRecordRecyclingDelayParam.java (revision 0) +++ ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/param/UseRecordRecyclingDelayParam.java (revision 41245) @@ -0,0 +1,87 @@ +package com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.param; + +import java.util.Objects; + +public class UseRecordRecyclingDelayParam { + private String startDay; + private String endDay; + private String operation; + private String doctorName; + private String tousseName; + private String departCode; + + public String getStartDay() { + return startDay; + } + + public void setStartDay(String startDay) { + this.startDay = startDay; + } + + public String getEndDay() { + return endDay; + } + + public void setEndDay(String endDay) { + this.endDay = endDay; + } + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public String getDoctorName() { + return doctorName; + } + + public void setDoctorName(String doctorName) { + this.doctorName = doctorName; + } + + public String getTousseName() { + return tousseName; + } + + public void setTousseName(String tousseName) { + this.tousseName = tousseName; + } + + public String getDepartCode() { + return departCode; + } + + public void setDepartCode(String departCode) { + this.departCode = departCode; + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof UseRecordRecyclingDelayParam)) return false; + UseRecordRecyclingDelayParam that = (UseRecordRecyclingDelayParam) object; + return Objects.equals(startDay, that.startDay) && Objects.equals(endDay, that.endDay) + && Objects.equals(operation, that.operation) && Objects.equals(doctorName, that.doctorName) + && Objects.equals(tousseName, that.tousseName) && Objects.equals(departCode, that.departCode); + } + + @Override + public int hashCode() { + return Objects.hash(startDay, endDay, operation, doctorName, tousseName, departCode); + } + + @Override + public String toString() { + return "UseRecordRecyclingDelayParam{" + + "startDay='" + startDay + '\'' + + ", endDay='" + endDay + '\'' + + ", operation='" + operation + '\'' + + ", doctorName='" + doctorName + '\'' + + ", tousseName='" + tousseName + '\'' + + ", departCode='" + departCode + '\'' + + '}'; + } +} Index: ssts-reports/src/test/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/handler/UseRecordRecyclingDelayHandlerClassifyTest.java =================================================================== diff -u --- ssts-reports/src/test/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/handler/UseRecordRecyclingDelayHandlerClassifyTest.java (revision 0) +++ ssts-reports/src/test/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/handler/UseRecordRecyclingDelayHandlerClassifyTest.java (revision 41245) @@ -0,0 +1,136 @@ +package com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.handler; + +import com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.model.TousseInstanceInfo; +import com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.model.UseRecordRecyclingDelayReportVo; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.MockitoAnnotations; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + +import static org.junit.Assert.*; + +/** + * UseRecordRecyclingDelayHandler 中 classify 方法的单元测试 + */ +public class UseRecordRecyclingDelayHandlerClassifyTest { + + @InjectMocks + private UseRecordRecyclingDelayHandlerImpl handler; + + private AutoCloseable autoCloseable; + + @Before + public void setUp() { + autoCloseable = MockitoAnnotations.openMocks(this); + } + + @After + public void tearDown() throws Exception { + if (autoCloseable != null) { + autoCloseable.close(); + } + } + + /** + * 测试输入为空时返回空map + */ + @Test + public void testClassify_EmptyList_ReturnsEmptyMap() { + List input = Collections.emptyList(); + Map result = handler.classify(input); + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + /** + * 测试多条记录属于同一日期的情况 + */ + @Test + public void testClassify_MultipleRecordsSameDate_CorrectlyGroupedAndCounted() throws ParseException { + List input = new ArrayList<>(); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + TousseInstanceInfo info1 = new TousseInstanceInfo(); + info1.setEnteringDate(sdf.parse("2023-01-01 10:00:00")); + info1.setStatus("已回收"); + info1.setRecyclingTime(sdf.parse("2023-01-01 10:25:00")); // delayType = 0 + + TousseInstanceInfo info2 = new TousseInstanceInfo(); + info2.setEnteringDate(sdf.parse("2023-01-01 11:00:00")); + info2.setStatus("已回收"); + info2.setRecyclingTime(sdf.parse("2023-01-01 11:45:00")); // delayType = 1 + + TousseInstanceInfo info3 = new TousseInstanceInfo(); + info3.setEnteringDate(sdf.parse("2023-01-01 12:02:00")); + info3.setStatus("已回收"); + info3.setRecyclingTime(sdf.parse("2023-01-01 13:02:00")); // delayType = 2 + + TousseInstanceInfo info4 = new TousseInstanceInfo(); + info4.setEnteringDate(sdf.parse("2023-01-01 11:05:00")); + info4.setStatus("已发货"); + info4.setRecyclingTime(null); // delayType = 3 + + TousseInstanceInfo info5 = new TousseInstanceInfo(); + info5.setEnteringDate(sdf.parse("2023-01-01 11:05:00")); + info5.setStatus("已回收"); + info5.setRecyclingTime(sdf.parse("2023-01-01 13:05:00")); // delayType = 3 + + input.addAll(Arrays.asList(info1, info2, info3, info4, info5)); + + Map result = handler.classify(input); + + assertEquals(1, result.size()); + + UseRecordRecyclingDelayReportVo vo = result.get("2023-01-01"); + assertNotNull(vo); + assertEquals(Integer.valueOf(1), vo.getHalfHourRecycledCount()); + assertEquals(Integer.valueOf(1), vo.getOneHourRecycledCount()); + assertEquals(Integer.valueOf(2), vo.getTwoHourRecycledCount()); + assertEquals(5, vo.getTousseInstances().size()); + } + + /** + * 测试多个不同日期的数据正确分组 + */ + @Test + public void testClassify_DifferentDates_CorrectlyGroupedByDate() throws ParseException { + List input = new ArrayList<>(); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + TousseInstanceInfo info1 = new TousseInstanceInfo(); + info1.setEnteringDate(sdf.parse("2023-01-01 10:00:00")); + info1.setStatus("已回收"); + info1.setRecyclingTime(sdf.parse("2023-01-01 10:35:00")); // delayType = 1 + + TousseInstanceInfo info2 = new TousseInstanceInfo(); + info2.setEnteringDate(sdf.parse("2023-01-02 11:00:00")); + info2.setStatus("已回收"); + info2.setRecyclingTime(sdf.parse("2023-01-02 12:45:00")); // delayType = 2 + + input.add(info1); + input.add(info2); + + Map result = handler.classify(input); + + assertEquals(2, result.size()); + + UseRecordRecyclingDelayReportVo vo1 = result.get("2023-01-01"); + UseRecordRecyclingDelayReportVo vo2 = result.get("2023-01-02"); + + assertNotNull(vo1); + assertNotNull(vo2); + + assertEquals(Integer.valueOf(1), vo1.getHalfHourRecycledCount()); + assertEquals(Integer.valueOf(0), vo1.getOneHourRecycledCount()); + + assertEquals(Integer.valueOf(0), vo2.getHalfHourRecycledCount()); + assertEquals(Integer.valueOf(1), vo2.getOneHourRecycledCount()); + } +} Index: ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/controller/RecyclingDelayController.java =================================================================== diff -u --- ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/controller/RecyclingDelayController.java (revision 0) +++ ssts-reports/src/main/java/com/forgon/disinfectsystem/reportforms/modules/recyclingdelay/controller/RecyclingDelayController.java (revision 41245) @@ -0,0 +1,38 @@ +package com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.controller; + +import com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.handler.UseRecordRecyclingDelayHandler; +import com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.handler.UseRecordRecyclingDelayHandlerImpl; +import com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.model.UseRecordRecyclingDelayReportVo; +import com.forgon.disinfectsystem.reportforms.modules.recyclingdelay.param.UseRecordRecyclingDelayParam; +import com.forgon.tools.json.JSONUtil; +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/disinfectSystem/reports/recyclingdelay") +public class RecyclingDelayController { + private final UseRecordRecyclingDelayHandler useRecordRecyclingDelayHandler; + + @Autowired + public RecyclingDelayController(UseRecordRecyclingDelayHandlerImpl useRecordRecyclingDelayHandler) { + this.useRecordRecyclingDelayHandler = useRecordRecyclingDelayHandler; + } + + @RequestMapping(method = RequestMethod.GET, produces = "application/json; charset=UTF-8") + public String loadUseRecordRecyclingDelayList(UseRecordRecyclingDelayParam param) { + JSONObject result; + try { + List list =useRecordRecyclingDelayHandler.loadUseRecordRecyclingDelayList(param); + result = JSONUtil.buildJsonObject(true, JSONArray.fromObject(list)); + } catch (Exception e) { + result = JSONUtil.buildJsonObject(false, "加载失败:" + e.getMessage()); + } + return result.toString(); + } +}