|
@@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.system.dal.dataobject.tenant.GroupTenantDO;
|
|
|
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
|
|
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
|
|
|
import cn.iocoder.yudao.module.system.dal.mysql.biz.OperationAnalysisMapper;
|
|
import cn.iocoder.yudao.module.system.dal.mysql.biz.OperationAnalysisMapper;
|
|
|
import cn.iocoder.yudao.module.system.dal.mysql.biz.bo.OperationNurseLevelCountBO;
|
|
import cn.iocoder.yudao.module.system.dal.mysql.biz.bo.OperationNurseLevelCountBO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.dal.mysql.biz.bo.OperationTenantBedCountBO;
|
|
|
import cn.iocoder.yudao.module.system.dal.mysql.biz.bo.OperationTenantCountBO;
|
|
import cn.iocoder.yudao.module.system.dal.mysql.biz.bo.OperationTenantCountBO;
|
|
|
import cn.iocoder.yudao.module.system.dal.mysql.biz.bo.OperationTenantMonthCountBO;
|
|
import cn.iocoder.yudao.module.system.dal.mysql.biz.bo.OperationTenantMonthCountBO;
|
|
|
import cn.iocoder.yudao.module.system.dal.mysql.tenant.GroupTenantMapper;
|
|
import cn.iocoder.yudao.module.system.dal.mysql.tenant.GroupTenantMapper;
|
|
@@ -22,11 +23,15 @@ import org.springframework.stereotype.Service;
|
|
|
import org.springframework.validation.annotation.Validated;
|
|
import org.springframework.validation.annotation.Validated;
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
import javax.annotation.Resource;
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
|
|
+import java.math.RoundingMode;
|
|
|
|
|
+import java.time.DayOfWeek;
|
|
|
import java.time.LocalDate;
|
|
import java.time.LocalDate;
|
|
|
import java.time.LocalDateTime;
|
|
import java.time.LocalDateTime;
|
|
|
import java.time.YearMonth;
|
|
import java.time.YearMonth;
|
|
|
import java.time.ZoneId;
|
|
import java.time.ZoneId;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.time.temporal.TemporalAdjusters;
|
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
|
import java.util.Arrays;
|
|
import java.util.Arrays;
|
|
|
import java.util.Collections;
|
|
import java.util.Collections;
|
|
@@ -50,7 +55,11 @@ import java.util.stream.Collectors;
|
|
|
* <li>轮候待住 —— 轮候入住 {@code status = 10}(审批完成待入住);</li>
|
|
* <li>轮候待住 —— 轮候入住 {@code status = 10}(审批完成待入住);</li>
|
|
|
* <li>预订在途 —— 床位预约 {@code status = 2}(已缴费);</li>
|
|
* <li>预订在途 —— 床位预约 {@code status = 2}(已缴费);</li>
|
|
|
* <li>近 12 个月已完成退住趋势 —— 按月统计「已完成」的退住,含当月,无数据补 0;</li>
|
|
* <li>近 12 个月已完成退住趋势 —— 按月统计「已完成」的退住,含当月,无数据补 0;</li>
|
|
|
- * <li>护理等级分布 —— 在住长者({@code in_status = 1})按护理等级分组。</li>
|
|
|
|
|
|
|
+ * <li>护理等级分布 —— 在住长者({@code in_status = 1})按护理等级分组;</li>
|
|
|
|
|
+ * <li>运营周期汇总 —— 本周(上周六 ~ 本周五)与本月(1 号 ~ 今天)的入住 / 退住 / 净增长,
|
|
|
|
|
+ * 期初数由「期末数 - 本周净增长」倒推,期末数取当前在住人数;</li>
|
|
|
|
|
+ * <li>院区床位结构 —— 机构下 {@code org_build_bed} 的总床位、在住床位(status = 1)、
|
|
|
|
|
+ * 空闲床位(status = 0)、临时占用(status = 2),入住率 = 在住床位 / 总床位。</li>
|
|
|
* </ol>
|
|
* </ol>
|
|
|
*
|
|
*
|
|
|
* @author kyj
|
|
* @author kyj
|
|
@@ -95,12 +104,17 @@ public class OperationAnalysisServiceImpl implements OperationAnalysisService {
|
|
|
|
|
|
|
|
LocalDate today = LocalDate.now();
|
|
LocalDate today = LocalDate.now();
|
|
|
Date dayBegin = toDate(today.atStartOfDay());
|
|
Date dayBegin = toDate(today.atStartOfDay());
|
|
|
- Date dayEnd = toDate(today.plusDays(1).atStartOfDay().minusNanos(1));
|
|
|
|
|
|
|
+ Date dayEnd = toDayEnd(today);
|
|
|
Date now = new Date();
|
|
Date now = new Date();
|
|
|
// 近 12 个月(含当月):起始月 1 号(含)~ 次月 1 号(不含)
|
|
// 近 12 个月(含当月):起始月 1 号(含)~ 次月 1 号(不含)
|
|
|
YearMonth currentMonth = YearMonth.now();
|
|
YearMonth currentMonth = YearMonth.now();
|
|
|
LocalDate trendBegin = currentMonth.minusMonths(TREND_MONTHS - 1).atDay(1);
|
|
LocalDate trendBegin = currentMonth.minusMonths(TREND_MONTHS - 1).atDay(1);
|
|
|
LocalDate trendEnd = currentMonth.plusMonths(1).atDay(1);
|
|
LocalDate trendEnd = currentMonth.plusMonths(1).atDay(1);
|
|
|
|
|
+ // 本周:上周六(含)~ 本周五(含);本月:1 号(含)~ 今天(含)
|
|
|
|
|
+ LocalDate weekBegin = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.SATURDAY));
|
|
|
|
|
+ LocalDate weekEnd = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY));
|
|
|
|
|
+ LocalDate monthBegin = today.withDayOfMonth(1);
|
|
|
|
|
+ LocalDate monthEnd = today;
|
|
|
|
|
|
|
|
// 1、六个指标:按机构分组,各一次查询
|
|
// 1、六个指标:按机构分组,各一次查询
|
|
|
Map<Long, Long> newCheckInMap = toTenantCountMap(
|
|
Map<Long, Long> newCheckInMap = toTenantCountMap(
|
|
@@ -116,6 +130,23 @@ public class OperationAnalysisServiceImpl implements OperationAnalysisService {
|
|
|
Map<Long, Long> reservationMap = toTenantCountMap(
|
|
Map<Long, Long> reservationMap = toTenantCountMap(
|
|
|
operationAnalysisMapper.selectReservationCount(tenantIds));
|
|
operationAnalysisMapper.selectReservationCount(tenantIds));
|
|
|
|
|
|
|
|
|
|
+ // 1.1、运营周期汇总:本周 / 本月的入住与退住、当前在住人数
|
|
|
|
|
+ Map<Long, Long> weeklyCheckInMap = toTenantCountMap(operationAnalysisMapper
|
|
|
|
|
+ .selectRangeCheckInCount(tenantIds, toDate(weekBegin.atStartOfDay()), toDayEnd(weekEnd)));
|
|
|
|
|
+ Map<Long, Long> weeklyCheckOutMap = toTenantCountMap(operationAnalysisMapper
|
|
|
|
|
+ .selectRangeCheckOutCount(tenantIds, weekBegin, weekEnd));
|
|
|
|
|
+ Map<Long, Long> monthlyCheckInMap = toTenantCountMap(operationAnalysisMapper
|
|
|
|
|
+ .selectRangeCheckInCount(tenantIds, toDate(monthBegin.atStartOfDay()), toDayEnd(monthEnd)));
|
|
|
|
|
+ Map<Long, Long> monthlyCheckOutMap = toTenantCountMap(operationAnalysisMapper
|
|
|
|
|
+ .selectRangeCheckOutCount(tenantIds, monthBegin, monthEnd));
|
|
|
|
|
+ Map<Long, Long> inStatusMap = toTenantCountMap(operationAnalysisMapper.selectInStatusCount(tenantIds));
|
|
|
|
|
+
|
|
|
|
|
+ // 1.2、院区床位结构:按机构分组,一次查询
|
|
|
|
|
+ Map<Long, OperationTenantBedCountBO> bedStructureMap = operationAnalysisMapper
|
|
|
|
|
+ .selectBedStructure(tenantIds).stream()
|
|
|
|
|
+ .filter(bo -> bo.getTenantId() != null)
|
|
|
|
|
+ .collect(Collectors.toMap(OperationTenantBedCountBO::getTenantId, bo -> bo, (a, b) -> a));
|
|
|
|
|
+
|
|
|
// 2、趋势与分布:先按机构分组
|
|
// 2、趋势与分布:先按机构分组
|
|
|
Map<Long, List<OperationTenantMonthCountBO>> trendGroup = operationAnalysisMapper
|
|
Map<Long, List<OperationTenantMonthCountBO>> trendGroup = operationAnalysisMapper
|
|
|
.selectRetreatTrend(tenantIds, trendBegin, trendEnd).stream()
|
|
.selectRetreatTrend(tenantIds, trendBegin, trendEnd).stream()
|
|
@@ -138,6 +169,9 @@ public class OperationAnalysisServiceImpl implements OperationAnalysisService {
|
|
|
vo.setTodayAskLeaveCount(todayAskLeaveMap.getOrDefault(tenantId, 0L));
|
|
vo.setTodayAskLeaveCount(todayAskLeaveMap.getOrDefault(tenantId, 0L));
|
|
|
vo.setWaitingCount(waitingMap.getOrDefault(tenantId, 0L));
|
|
vo.setWaitingCount(waitingMap.getOrDefault(tenantId, 0L));
|
|
|
vo.setReservationCount(reservationMap.getOrDefault(tenantId, 0L));
|
|
vo.setReservationCount(reservationMap.getOrDefault(tenantId, 0L));
|
|
|
|
|
+ fillOperationSummary(vo, weeklyCheckInMap, weeklyCheckOutMap, monthlyCheckInMap,
|
|
|
|
|
+ monthlyCheckOutMap, inStatusMap, weekBegin, weekEnd);
|
|
|
|
|
+ fillBedStructure(vo, bedStructureMap.get(tenantId));
|
|
|
vo.setRetreatTrend(buildRetreatTrend(trendGroup.get(tenantId)));
|
|
vo.setRetreatTrend(buildRetreatTrend(trendGroup.get(tenantId)));
|
|
|
vo.setNurseLevelDistribution(buildNurseLevelDistribution(nurseLevelGroup.get(tenantId)));
|
|
vo.setNurseLevelDistribution(buildNurseLevelDistribution(nurseLevelGroup.get(tenantId)));
|
|
|
result.add(vo);
|
|
result.add(vo);
|
|
@@ -145,6 +179,86 @@ public class OperationAnalysisServiceImpl implements OperationAnalysisService {
|
|
|
return result;
|
|
return result;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 填充运营周期汇总:本周 / 本月的入住、退住、净增长与期初 / 期末数
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param vo 待填充的机构数据
|
|
|
|
|
+ * @param weeklyCheckInMap 本周入住数
|
|
|
|
|
+ * @param weeklyCheckOutMap 本周退住数
|
|
|
|
|
+ * @param monthlyCheckInMap 本月入住数
|
|
|
|
|
+ * @param monthlyCheckOutMap 本月退住数
|
|
|
|
|
+ * @param inStatusMap 当前在住人数
|
|
|
|
|
+ * @param weekBegin 本周开始日期
|
|
|
|
|
+ * @param weekEnd 本周结束日期
|
|
|
|
|
+ */
|
|
|
|
|
+ private void fillOperationSummary(OperationAnalysisOverviewDynamicsRespVO vo,
|
|
|
|
|
+ Map<Long, Long> weeklyCheckInMap,
|
|
|
|
|
+ Map<Long, Long> weeklyCheckOutMap,
|
|
|
|
|
+ Map<Long, Long> monthlyCheckInMap,
|
|
|
|
|
+ Map<Long, Long> monthlyCheckOutMap,
|
|
|
|
|
+ Map<Long, Long> inStatusMap,
|
|
|
|
|
+ LocalDate weekBegin, LocalDate weekEnd) {
|
|
|
|
|
+ Long tenantId = vo.getTenantId();
|
|
|
|
|
+ Long weeklyCheckIn = weeklyCheckInMap.getOrDefault(tenantId, 0L);
|
|
|
|
|
+ Long weeklyCheckOut = weeklyCheckOutMap.getOrDefault(tenantId, 0L);
|
|
|
|
|
+ Long monthlyCheckIn = monthlyCheckInMap.getOrDefault(tenantId, 0L);
|
|
|
|
|
+ Long monthlyCheckOut = monthlyCheckOutMap.getOrDefault(tenantId, 0L);
|
|
|
|
|
+
|
|
|
|
|
+ // 期末数取当前在住人数,期初数由「期末数 - 本周净增长」倒推
|
|
|
|
|
+ Long periodEndCount = inStatusMap.getOrDefault(tenantId, 0L);
|
|
|
|
|
+ Long weeklyNetGrowth = weeklyCheckIn - weeklyCheckOut;
|
|
|
|
|
+ Long periodBeginCount = Math.max(0L, periodEndCount - weeklyNetGrowth);
|
|
|
|
|
+
|
|
|
|
|
+ vo.setPeriodBegin(weekBegin);
|
|
|
|
|
+ vo.setPeriodEnd(weekEnd);
|
|
|
|
|
+ vo.setPeriodBeginCount(periodBeginCount);
|
|
|
|
|
+ vo.setWeeklyCheckInCount(weeklyCheckIn);
|
|
|
|
|
+ vo.setWeeklyCheckOutCount(weeklyCheckOut);
|
|
|
|
|
+ vo.setPeriodEndCount(periodEndCount);
|
|
|
|
|
+ vo.setWeeklyNetGrowthCount(weeklyNetGrowth);
|
|
|
|
|
+ vo.setMonthlyCheckInCount(monthlyCheckIn);
|
|
|
|
|
+ vo.setMonthlyCheckOutCount(monthlyCheckOut);
|
|
|
|
|
+ vo.setMonthlyNetGrowthCount(monthlyCheckIn - monthlyCheckOut);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 填充院区床位结构:总床位、在住床位、入住率、空闲床位、临时占用
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param vo 待填充的机构数据
|
|
|
|
|
+ * @param bed 该机构的床位统计,可能为 null(表示无床位数据)
|
|
|
|
|
+ */
|
|
|
|
|
+ private void fillBedStructure(OperationAnalysisOverviewDynamicsRespVO vo, OperationTenantBedCountBO bed) {
|
|
|
|
|
+ Long totalBedCount = bed == null ? 0L : nullToZero(bed.getTotalBedCount());
|
|
|
|
|
+ Long occupiedBedCount = bed == null ? 0L : nullToZero(bed.getOccupiedBedCount());
|
|
|
|
|
+ Long emptyBedCount = bed == null ? 0L : nullToZero(bed.getEmptyBedCount());
|
|
|
|
|
+ Long temporaryOccupationCount = bed == null ? 0L : nullToZero(bed.getTemporaryOccupationCount());
|
|
|
|
|
+
|
|
|
|
|
+ vo.setTotalBedCount(totalBedCount);
|
|
|
|
|
+ vo.setOccupiedBedCount(occupiedBedCount);
|
|
|
|
|
+ vo.setOccupancyRate(calcOccupancyRate(occupiedBedCount, totalBedCount));
|
|
|
|
|
+ vo.setEmptyBedCount(emptyBedCount);
|
|
|
|
|
+ vo.setTemporaryOccupationCount(temporaryOccupationCount);
|
|
|
|
|
+ // 本周期末院内空余床位数,与空闲床位数口径一致
|
|
|
|
|
+ vo.setPeriodEndEmptyBedCount(emptyBedCount);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 计算入住率:在住床位 / 总床位,保留 2 位小数,如 {@code 75.84%}
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param occupiedBedCount 在住床位数
|
|
|
|
|
+ * @param totalBedCount 总床位数
|
|
|
|
|
+ * @return 入住率百分比字符串,总床位为 0 时返回 {@code 0.00%}
|
|
|
|
|
+ */
|
|
|
|
|
+ private static String calcOccupancyRate(Long occupiedBedCount, Long totalBedCount) {
|
|
|
|
|
+ if (totalBedCount == null || totalBedCount <= 0) {
|
|
|
|
|
+ return "0.00%";
|
|
|
|
|
+ }
|
|
|
|
|
+ return BigDecimal.valueOf(nullToZero(occupiedBedCount))
|
|
|
|
|
+ .multiply(BigDecimal.valueOf(100))
|
|
|
|
|
+ .divide(BigDecimal.valueOf(totalBedCount), 2, RoundingMode.HALF_UP)
|
|
|
|
|
+ .toPlainString() + "%";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 解析要统计的机构ID:优先使用入参,否则取当前登录机构;集团账号展开为旗下所有机构
|
|
* 解析要统计的机构ID:优先使用入参,否则取当前登录机构;集团账号展开为旗下所有机构
|
|
|
*
|
|
*
|
|
@@ -243,6 +357,16 @@ public class OperationAnalysisServiceImpl implements OperationAnalysisService {
|
|
|
return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
|
|
return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 取当天最后一刻(23:59:59.999999999)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param date 日期
|
|
|
|
|
+ * @return 当天最后一刻
|
|
|
|
|
+ */
|
|
|
|
|
+ private static Date toDayEnd(LocalDate date) {
|
|
|
|
|
+ return toDate(date.plusDays(1).atStartOfDay().minusNanos(1));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private static Long nullToZero(Long value) {
|
|
private static Long nullToZero(Long value) {
|
|
|
return value == null ? 0L : value;
|
|
return value == null ? 0L : value;
|
|
|
}
|
|
}
|