ruoyi-admin/src/main/java/com/ruoyi/web/controller/smartor/ServiceSubtaskController.java
@@ -302,8 +302,7 @@ public AjaxResult querySubtaskList(@RequestBody ServiceSubtaskVO serviceSubtaskVO) { ServiceSubtaskEntity serviceSubtaskEntity = DtoConversionUtils.sourceToTarget(serviceSubtaskVO, ServiceSubtaskEntity.class); serviceSubtaskEntity.setContinueContent(ObjectUtils.isNotEmpty(serviceSubtaskVO.getContinueContent()) ? serviceSubtaskVO.getContinueContent().toString() : null); PageUtils.startPageByPost(serviceSubtaskEntity.getPageNum(), serviceSubtaskEntity.getPageSize()); serviceSubtaskService.selectServiceSubtaskList(serviceSubtaskEntity); // 该 SQL 已内置 limit ${pageSize} OFFSET ${pageNum},不再走 PageHelper,避免两套分页叠加产生两个 LIMIT return success(serviceSubtaskService.selectServiceSubtaskList(serviceSubtaskEntity)); } @@ -317,8 +316,8 @@ public TableDataInfo querySubtaskListAgain(@RequestBody ServiceSubtaskVO serviceSubtaskVO) { ServiceSubtaskEntity serviceSubtaskEntity = DtoConversionUtils.sourceToTarget(serviceSubtaskVO, ServiceSubtaskEntity.class); serviceSubtaskEntity.setContinueContent(ObjectUtils.isNotEmpty(serviceSubtaskVO.getContinueContent()) ? serviceSubtaskVO.getContinueContent().toString() : null); // 该 SQL 未内置 limit,使用 PageHelper 分页;只调用一次,getDataTable 可基于 Page 取到正确 total PageUtils.startPageByPost(serviceSubtaskEntity.getPageNum(), serviceSubtaskEntity.getPageSize()); serviceSubtaskService.selectServiceSubtaskListAgain(serviceSubtaskEntity); return getDataTable(serviceSubtaskService.selectServiceSubtaskListAgain(serviceSubtaskEntity)); } @@ -833,17 +832,20 @@ } /** * 统计任务各种状态的数量(优化版) * 统计任务各种状态的数量(优化版)-【随访工作台】 * * @param soonTimeoutHours 即将超时时间(小时),默认 48 * @return Map<String, Object> 包含各状态统计数据 * pendingVisitCount: 当前登录人截止今日待随访总量 * failedVisitCount: 当前登录人随访任务失败总量 * abnormalVisitVount: 当前登录人随访任务异常总量 * allVisitCount: 当前登录人随访任务总量 * soonTimeoutCount: 当前登录人即将超时数量(超时截止时间在 soonTimeoutHours 小时内) */ @ApiOperation("获取当前用户随访数量统计") @PostMapping("/getCurrentUserServiceSubtaskCount") public Map<String, Object> getCurrentUserServiceSubtaskCount() { public Map<String, Object> getCurrentUserServiceSubtaskCount( @RequestParam(value = "soonTimeoutHours", required = false, defaultValue = "48") Integer soonTimeoutHours) { LoginUser loginUser = SecurityUtils.getLoginUser(); if (loginUser == null || loginUser.getUser() == null) { return new HashMap<>(); @@ -863,6 +865,7 @@ entity.setOrgid(sysUser.getOrgid()); entity.setLeaveldeptcodes(deptCodes); entity.setLeavehospitaldistrictcodes(wardCodes); entity.setSoonTimeoutHours(soonTimeoutHours); // admin 查看全院;非 admin 只要配置了科室或病区任一维度即可查询(SQL 中为 OR 关系) boolean isAdmin = "admin".equals(sysUser.getUserName()); if (isAdmin || CollectionUtils.isNotEmpty(deptCodes) || CollectionUtils.isNotEmpty(wardCodes)) { @@ -875,6 +878,7 @@ empty.put("abnormalVisitCount", 0); empty.put("warnningVisitCount", 0); empty.put("allVisitCount", 0); empty.put("soonTimeoutCount", 0); return empty; } ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java
@@ -33,7 +33,7 @@ /** * 创建时间 */ @JsonFormat(pattern = "yyyy-MM-dd[ HH:mm:ss]", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date createTime; /** @@ -44,7 +44,7 @@ /** * 更新时间 */ @JsonFormat(pattern = "yyyy-MM-dd[ HH:mm:ss]", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date updateTime; /** ruoyi-common/src/main/java/com/ruoyi/common/utils/TreeUtils.java
@@ -76,13 +76,41 @@ * 根据 SysDept.parentId 构造部门树 */ public static List<SysDept> buildDeptTree(List<SysDept> depts) { // 按 deptId 去重,防止 sys_user_dept 一对多关联产生重复的父节点,导致构建部门树时出现问题 depts = distinctByDeptId(depts); return buildTree(depts, SysDept::getDeptId, SysDept::getParentId, SysDept::setChildren); } /** * 一次性获取所有部门叶子节点 * 从扁平部门列表一次性获取所有叶子部门:即「deptId 不出现在任何节点的 parentId 中」的节点。 * 入参应为扁平列表(不是 buildDeptTree 的返回值),无需先建树。 * 会先按 deptId 去重,避免 join 产生的重复部门变成重复叶子。 */ public static List<SysDept> getLeafDepts(List<SysDept> depts) { return getLeafNodes(depts, SysDept::getHisDeptId, SysDept::getHisParentId); return getLeafNodes(distinctByDeptId(depts), SysDept::getDeptId, SysDept::getParentId); } /** * 按 deptId 对部门列表去重,保留首次出现的节点。 * 查询结果通过 dept_code 关联 sys_user_dept,同一部门可能出现多条记录(重复的父节点), * 构建部门树前需去重,避免重复节点导致树结构异常。 * * @param list 部门列表,允许为 null * @return 去重后的部门列表;入参为 null 或空时原样返回 */ private static List<SysDept> distinctByDeptId(List<SysDept> list) { if (list == null || list.isEmpty()) { return list; } Set<Long> deptIdSet = new HashSet<>(); List<SysDept> distinctList = new ArrayList<>(list.size()); for (SysDept dept : list) { Long deptId = dept.getDeptId(); // deptId 为空时保留,避免误合并不同部门 if (deptId == null || deptIdSet.add(deptId)) { distinctList.add(dept); } } return distinctList; } } ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDeptServiceImpl.java
@@ -335,8 +335,7 @@ public List<SysDept> selectLeafSysDeptListBySysUserDept(SysUserDept sysUserDept){ List<SysDept> list = deptMapper.selectSysDeptListBySysUserDept(sysUserDept); if(CollectionUtils.isNotEmpty(list)){ List<SysDept> tree = TreeUtils.buildDeptTree(list); list = TreeUtils.getLeafDepts(tree); list = TreeUtils.getLeafDepts(list); } return list; } ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
@@ -982,8 +982,7 @@ public List<SysUserDept> selectLeafSysDeptList(SysUserDept sysUserDept){ List<SysDept> list = sysDeptMapper.selectSysDeptListBySysUserDept(sysUserDept); if(org.apache.commons.collections4.CollectionUtils.isNotEmpty(list)){ List<SysDept> tree = TreeUtils.buildDeptTree(list); list = TreeUtils.getLeafDepts(tree); list = TreeUtils.getLeafDepts(list); } List<SysUserDept> list1 = DtoConversionUtils.sourceToTarget(list, SysUserDept.class); return list1; @@ -993,8 +992,7 @@ SysUserDept sysUserDept = DtoConversionUtils.sourceToTarget(sysDept, SysUserDept.class); List<SysDept> list = sysDeptMapper.selectSysDeptListBySysUserDept(sysUserDept); if(org.apache.commons.collections4.CollectionUtils.isNotEmpty(list)){ List<SysDept> tree = TreeUtils.buildDeptTree(list); list = TreeUtils.getLeafDepts(tree); list = TreeUtils.getLeafDepts(list); } return list; } smartor/src/main/java/com/smartor/domain/PatMaternityArchive.java
@@ -60,7 +60,7 @@ private String parentname; /** 出生日期 */ @JsonFormat(pattern = "yyyy-MM-dd[ HH:mm:ss]", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @Excel(name = "出生日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") @ApiModelProperty(value = "出生日期") private Date birthdate; @@ -76,19 +76,19 @@ private String phone; /** 诊断时间 */ @JsonFormat(pattern = "yyyy-MM-dd[ HH:mm:ss]", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @Excel(name = "诊断时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") @ApiModelProperty(value = "诊断时间") private Date diagTime; /** 建档时间 */ @JsonFormat(pattern = "yyyy-MM-dd[ HH:mm:ss]", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @Excel(name = "建档时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") @ApiModelProperty(value = "建档时间") private Date archiveTime; /** 拟随访时间 */ @JsonFormat(pattern = "yyyy-MM-dd[ HH:mm:ss]", timezone = "GMT+8") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @Excel(name = "拟随访时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") @ApiModelProperty(value = "拟随访时间") private Date plannedFollowTime; smartor/src/main/java/com/smartor/domain/VO/ServiceSubtaskVO.java
@@ -806,6 +806,18 @@ @ApiModelProperty(value = "页面展示状态 :1:待随访(2 待发送);2:随访中(3 已发送、1 被领取)、3:未完成(5 发送失败、7、超时)、4:已完成( 6 已完成)、5:无需随访(4 不执行)") private Long sendstateView; /** * 是否即将超时:1 是(配合 soonTimeoutHours 使用,用于查询即将超时列表) */ @ApiModelProperty(value = "是否即将超时:1是") private Integer soonTimeout; /** * 即将超时时间(小时):截止时间在 N 小时内 */ @ApiModelProperty(value = "即将超时时间(小时)") private Integer soonTimeoutHours; // /** // * 日期限制 目前由前端控制 // */ smartor/src/main/java/com/smartor/domain/entity/ServiceSubtaskEntity.java
@@ -114,6 +114,18 @@ private Integer rateDay; /** * 是否即将超时:1 是(配合 soonTimeoutHours 使用) */ @ApiModelProperty(value = "是否即将超时:1是") private Integer soonTimeout; /** * 即将超时时间(小时):统计距超时截止时间在 N 小时内的待随访数量 */ @ApiModelProperty(value = "即将超时时间(小时)") private Integer soonTimeoutHours; /** * 性别 */ @Excel(name = " 性别") smartor/src/main/java/com/smartor/mapper/ServiceSubtaskMapper.java
@@ -38,6 +38,9 @@ /** * 查询单一任务(随访)列表 * <p> * 入参 soonTimeout=1 且 soonTimeoutHours=N 时,仅返回“尚未超时、且将在 N 小时内超时”的数据, * 截止时间口径与 getCurrentUserServiceSubtaskCount 的 soonTimeoutCount 一致。 * * @param serviceSubtaskVO 单一任务(随访) * @return 单一任务(随访)集合 @@ -227,6 +230,7 @@ * failedVisitCount: 当前登录人随访任务失败总量 * abnormalVisitVount: 当前登录人随访任务异常总量 * allVisitCount: 当前登录人随访任务总量 * soonTimeoutCount: 当前登录人即将超时数量(超时截止时间在 soonTimeoutHours 小时内,仅统计尚未超时的数据) */ public Map<String, Object> getCurrentUserServiceSubtaskCount(ServiceSubtaskEntity serviceSubtaskVO); smartor/src/main/java/com/smartor/service/IServiceSubtaskService.java
@@ -196,6 +196,13 @@ public List<ServiceSubtaskDiagname> convertToDiagnameList(List<ServiceSubtask> subtaskList); /** * 统计任务各种状态的数量 -【随访工作台】 * * @param entity 查询条件(科室/病区权限、soonTimeoutHours 即将超时小时数等) * @return 含 pendingVisitCount、failedVisitCount、abnormalVisitCount、warnningVisitCount、 * allVisitCount、soonTimeoutCount 的统计结果 */ public Map<String, Object> getCurrentUserServiceSubtaskCount(ServiceSubtaskEntity entity); /** smartor/src/main/resources/mapper/smartor/ServiceSubtaskMapper.xml
@@ -291,6 +291,36 @@ <if test="continueTimeNext != null ">and continue_time_next = #{continueTimeNext}</if> </select> <!-- 即将超时过滤:是否即将超时(soonTimeout=1) + 即将超时时间(soonTimeoutHours,单位小时)。 截止时间口径与 RyTask/RedisMqReceiver 的 isSubtaskDeadline 保持一致: service_subtask.endtime + service_task.send_day(缺省1) + (Σ service_subtask_preachform.compensate_time + 1) 天; 仅当存在有效补偿记录时才计算截止时间(无补偿记录不参与即将超时); 仅统计“尚未超时、且在 N 小时内将要超时”的数据。 --> <sql id="soonTimeoutDeadlineExpr"> ( SELECT DATE_ADD( DATE_ADD(ss.endtime, INTERVAL COALESCE(st.send_day, 1) DAY), INTERVAL (COALESCE(pf.compensate_days, 0) + 1) DAY ) FROM service_subtask ss LEFT JOIN service_task st ON st.taskid = ss.taskid AND st.del_flag = '0' INNER JOIN ( SELECT subid, SUM(CAST(COALESCE(NULLIF(TRIM(compensate_time), ''), '0') AS SIGNED)) AS compensate_days FROM service_subtask_preachform WHERE del_flag = 0 GROUP BY subid ) pf ON pf.subid = ss.id WHERE ss.id = service_subtask.id ) </sql> <sql id="soonTimeoutFilter"> <if test="soonTimeout != null and soonTimeout == 1"> AND <include refid="soonTimeoutDeadlineExpr"/> > NOW() AND <include refid="soonTimeoutDeadlineExpr"/> <= DATE_ADD(NOW(), INTERVAL COALESCE(#{soonTimeoutHours}, 0) HOUR) </if> </sql> <select id="selectServiceSubtaskList" parameterType="com.smartor.domain.entity.ServiceSubtaskEntity" resultMap="ServiceSubtaskResult"> @@ -529,6 +559,7 @@ #{visitDeptCode} </foreach> </if> <include refid="soonTimeoutFilter"/> <if test="sort != null and sort==0">order by endtime asc</if> <if test="sort != null and sort==1">order by endtime desc</if> <if test="sort != null and sort==2">order by visit_time asc</if> @@ -538,7 +569,9 @@ <if test="sort != null and sort==7">order by visit_time asc</if> <if test="sort != null and sort==8">order by visit_time desc</if> <if test="sort != null and sort==9">order by sendname asc</if> <if test="pageSize != null and pageNum != null">limit ${pageSize} OFFSET ${pageNum}</if> <if test="pageSize != null and pageNum != null"> LIMIT ${pageSize} OFFSET ${pageNum} </if> </select> <select id="selectSatisfactionSubtaskList" parameterType="com.smartor.domain.VO.PatSatisfactionReqVO" @@ -696,6 +729,7 @@ <if test="continueTimeNow != null ">and continue_time_now = #{continueTimeNow,jdbcType=TIMESTAMP}</if> <if test="continueCount != null ">and continue_count = #{continueCount}</if> <if test="continueTimeNext != null ">and continue_time_next = #{continueTimeNext}</if> <include refid="soonTimeoutFilter"/> </select> <select id="selectSendstateByCondition" parameterType="com.smartor.domain.ServiceSubtask" resultType="Integer"> @@ -2846,22 +2880,54 @@ COUNT(CASE WHEN sendstate = 5 THEN 1 END) AS failedVisitCount, SUM(CASE WHEN excep IS NOT NULL AND excep = '1' THEN 1 ELSE 0 END) AS abnormalVisitCount, SUM(CASE WHEN excep IS NOT NULL AND excep = '2' THEN 1 ELSE 0 END) AS warnningVisitCount, COUNT(*) AS allVisitCount FROM service_subtask WHERE del_flag = '0' AND service_type = '2' AND visit_count = 1 COUNT(*) AS allVisitCount, <choose> <when test="soonTimeoutHours != null and soonTimeoutHours > 0"> COUNT(CASE WHEN t.soon_deadline > NOW() AND t.soon_deadline <= DATE_ADD(NOW(), INTERVAL #{soonTimeoutHours} HOUR) THEN 1 END) AS soonTimeoutCount </when> <otherwise> 0 AS soonTimeoutCount </otherwise> </choose> FROM ( SELECT ss.*, <!-- 即将超时截止时间 = 出院时间(endtime) + 发送日(send_day, 缺省1) + 补偿天数(compensate_time 求和后再 +1 天), 与 RyTask/RedisMqReceiver 的 isSubtaskDeadline 口径保持一致; 无有效补偿记录(cp.subid 为空)时截止时间为 NULL,不参与即将超时统计 --> CASE WHEN cp.subid IS NULL THEN NULL ELSE DATE_ADD( DATE_ADD(ss.endtime, INTERVAL COALESCE(st.send_day, 1) DAY), INTERVAL (COALESCE(cp.compensate_days, 0) + 1) DAY ) END AS soon_deadline FROM service_subtask ss LEFT JOIN service_task st ON st.taskid = ss.taskid AND st.del_flag = '0' <!-- 补偿时间:先按子任务聚合成一行再关联,避免一对多放大计数; compensate_time 是 varchar 且可能存成空串,需先置空再转数字 --> LEFT JOIN ( SELECT subid, SUM(CAST(COALESCE(NULLIF(TRIM(compensate_time), ''), '0') AS SIGNED)) AS compensate_days FROM service_subtask_preachform WHERE del_flag = 0 GROUP BY subid ) cp ON cp.subid = ss.id WHERE ss.del_flag = '0' AND ss.service_type = '2' AND ss.visit_count = 1 <if test="orgid != null and orgid != ''"> AND orgid = #{orgid} AND ss.orgid = #{orgid} </if> <if test="campusid != null and campusid != ''"> and campusid = #{campusid} and ss.campusid = #{campusid} </if> <if test="(leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0) or (leaveldeptcodes != null and leaveldeptcodes.size() > 0)"> AND ( <if test="leavehospitaldistrictcodes != null and leavehospitaldistrictcodes.size() > 0"> leavehospitaldistrictcode IN ss.leavehospitaldistrictcode IN <foreach collection="leavehospitaldistrictcodes" item="leavehospitaldistrictcode" open="(" separator="," close=")"> @@ -2873,13 +2939,14 @@ OR </if> <if test=" leaveldeptcodes != null and leaveldeptcodes.size()>0"> deptcode IN ss.deptcode IN <foreach collection="leaveldeptcodes" item="leaveldeptcode" open="(" separator="," close=")"> #{leaveldeptcode} </foreach> </if>) </if> ) t </select> <select id="querySendCount" parameterType="com.smartor.domain.entity.ServiceSubtaskSatisfactionEntity" smartor/src/main/resources/mapper/smartor/ServiceTaskMapper.xml
@@ -462,6 +462,9 @@ <if test="uploadTime != null">upload_time,</if> <if test="orgid != null">orgid,</if> <if test="campusid != null and campusid != ''">campusid,</if> <if test="nexttaskflag != null">nexttaskflag,</if> <if test="nexttaskid != null">nexttaskid,</if> <if test="nexttaskname != null">nexttaskname,</if> <if test="pid != null">pid,</if> <if test="guid != null">guid,</if> <if test="preachform != null">preachform,</if> @@ -542,7 +545,7 @@ <if test="jsy != null and jsy != ''">#{jsy},</if> <if test="leavehospitaldistrictname != null ">#{leavehospitaldistrictname},</if> <if test="leavehospitaldistrictcode != null ">#{leavehospitaldistrictcode},</if> <if test="compensateDate != null and compensateDate != ''">#{compensateDate}</if> <if test="compensateDate != null and compensateDate != ''">#{compensateDate},</if> <if test="sendDay != null ">#{sendDay},</if> <if test="appltype != null ">#{appltype},</if> <if test="patCycle != null ">#{patCycle},</if>