|
@@ -16,6 +16,11 @@ import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.
|
|
|
import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisOverviewDynamicsRespVO;
|
|
import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisOverviewDynamicsRespVO;
|
|
|
import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisOverviewDynamicsRespVO.MonthlyRetreatTrendVO;
|
|
import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisOverviewDynamicsRespVO.MonthlyRetreatTrendVO;
|
|
|
import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisOverviewDynamicsRespVO.NurseLevelDistributionVO;
|
|
import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisOverviewDynamicsRespVO.NurseLevelDistributionVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisRetreatReasonReqVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisRetreatReasonRespVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisRetreatReasonRespVO.MonthlyReasonTrendVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisRetreatReasonRespVO.ReasonCountVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisRetreatReasonRespVO.ReasonStatVO;
|
|
|
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.ElderStructureRowBO;
|
|
import cn.iocoder.yudao.module.system.dal.mysql.biz.bo.ElderStructureRowBO;
|
|
@@ -23,6 +28,7 @@ import cn.iocoder.yudao.module.system.dal.mysql.biz.bo.OperationNurseLevelCountB
|
|
|
import cn.iocoder.yudao.module.system.dal.mysql.biz.bo.OperationTenantBedCountBO;
|
|
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.biz.bo.RetreatReasonRowBO;
|
|
|
import cn.iocoder.yudao.module.system.service.tenant.TenantService;
|
|
import cn.iocoder.yudao.module.system.service.tenant.TenantService;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.validation.annotation.Validated;
|
|
import org.springframework.validation.annotation.Validated;
|
|
@@ -90,6 +96,11 @@ public class OperationAnalysisServiceImpl implements OperationAnalysisService {
|
|
|
*/
|
|
*/
|
|
|
private static final String UNSET_LABEL = "未设置";
|
|
private static final String UNSET_LABEL = "未设置";
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 退住原因未填写时展示的名称
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final String UNFILLED_REASON = "未填写";
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 按日统计时最多返回的天数
|
|
* 按日统计时最多返回的天数
|
|
|
*/
|
|
*/
|
|
@@ -886,6 +897,193 @@ public class OperationAnalysisServiceImpl implements OperationAnalysisService {
|
|
|
.divide(BigDecimal.valueOf(count), 1, RoundingMode.HALF_UP);
|
|
.divide(BigDecimal.valueOf(count), 1, RoundingMode.HALF_UP);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ========== 退住原因与平均入住时长 ==========
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @TenantIgnore
|
|
|
|
|
+ public List<OperationAnalysisRetreatReasonRespVO> getRetreatReasonStats(
|
|
|
|
|
+ OperationAnalysisRetreatReasonReqVO reqVO) {
|
|
|
|
|
+ List<Long> tenantIds = tenantScopeService.resolveTenantIds(reqVO.getTenantIds());
|
|
|
|
|
+ if (CollUtil.isEmpty(tenantIds)) {
|
|
|
|
|
+ return Collections.emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
+ int orgType = reqVO.getOrgType() == null ? 1 : reqVO.getOrgType();
|
|
|
|
|
+ LocalDate[] range = resolveRetreatReasonRange(reqVO);
|
|
|
|
|
+ LocalDate begin = range[0];
|
|
|
|
|
+ LocalDate end = range[1];
|
|
|
|
|
+
|
|
|
|
|
+ List<RetreatReasonRowBO> rows =
|
|
|
|
|
+ operationAnalysisMapper.selectRetreatReasonRows(tenantIds, orgType, begin, end);
|
|
|
|
|
+
|
|
|
|
|
+ // 机构 -> 原因 -> 聚合;机构 -> 月份 -> 原因 -> 人数
|
|
|
|
|
+ Map<Long, Map<String, ReasonAgg>> tenantReasonMap = new LinkedHashMap<>();
|
|
|
|
|
+ Map<Long, Map<String, Map<String, Long>>> tenantMonthMap = new LinkedHashMap<>();
|
|
|
|
|
+ for (Long tenantId : tenantIds) {
|
|
|
|
|
+ tenantReasonMap.put(tenantId, new HashMap<>());
|
|
|
|
|
+ tenantMonthMap.put(tenantId, new LinkedHashMap<>());
|
|
|
|
|
+ }
|
|
|
|
|
+ for (RetreatReasonRowBO row : rows) {
|
|
|
|
|
+ if (row.getTenantId() == null) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ String reason = normalizeReason(row.getReason());
|
|
|
|
|
+ ReasonAgg agg = tenantReasonMap.computeIfAbsent(row.getTenantId(), k -> new HashMap<>())
|
|
|
|
|
+ .computeIfAbsent(reason, k -> new ReasonAgg());
|
|
|
|
|
+ agg.count++;
|
|
|
|
|
+ accumulateStay(agg, row);
|
|
|
|
|
+
|
|
|
|
|
+ if (row.getRetreatDate() != null) {
|
|
|
|
|
+ tenantMonthMap.computeIfAbsent(row.getTenantId(), k -> new LinkedHashMap<>())
|
|
|
|
|
+ .computeIfAbsent(row.getRetreatDate().format(MONTH_FORMATTER), k -> new HashMap<>())
|
|
|
|
|
+ .merge(reason, 1L, Long::sum);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<String> months = buildPeriods(begin, end, false);
|
|
|
|
|
+ List<OperationAnalysisRetreatReasonRespVO> result = new ArrayList<>(tenantIds.size());
|
|
|
|
|
+ for (Long tenantId : tenantIds) {
|
|
|
|
|
+ Map<String, ReasonAgg> reasonMap = tenantReasonMap.getOrDefault(tenantId, Collections.emptyMap());
|
|
|
|
|
+ long totalCount = 0L;
|
|
|
|
|
+ long stayDaySum = 0L;
|
|
|
|
|
+ long stayMonthSum = 0L;
|
|
|
|
|
+ long staySampleCount = 0L;
|
|
|
|
|
+ for (ReasonAgg agg : reasonMap.values()) {
|
|
|
|
|
+ totalCount += agg.count;
|
|
|
|
|
+ stayDaySum += agg.stayDaySum;
|
|
|
|
|
+ stayMonthSum += agg.stayMonthSum;
|
|
|
|
|
+ staySampleCount += agg.staySampleCount;
|
|
|
|
|
+ }
|
|
|
|
|
+ List<ReasonStatVO> reasonStats = buildReasonStats(reasonMap, totalCount);
|
|
|
|
|
+
|
|
|
|
|
+ OperationAnalysisRetreatReasonRespVO vo = new OperationAnalysisRetreatReasonRespVO();
|
|
|
|
|
+ vo.setTenantId(tenantId);
|
|
|
|
|
+ vo.setTenantName(getTenantName(tenantId));
|
|
|
|
|
+ vo.setStartDate(begin.format(DAY_FORMATTER));
|
|
|
|
|
+ vo.setEndDate(end.format(DAY_FORMATTER));
|
|
|
|
|
+ vo.setRetreatTotalCount(totalCount);
|
|
|
|
|
+ vo.setAvgStayDays(calcAvg(stayDaySum, staySampleCount));
|
|
|
|
|
+ vo.setAvgStayMonths(calcAvg(stayMonthSum, staySampleCount));
|
|
|
|
|
+ if (CollUtil.isNotEmpty(reasonStats)) {
|
|
|
|
|
+ ReasonStatVO top = reasonStats.get(0);
|
|
|
|
|
+ vo.setTopReason(top.getReason());
|
|
|
|
|
+ vo.setTopReasonCount(top.getCount());
|
|
|
|
|
+ vo.setTopReasonRate(top.getRate());
|
|
|
|
|
+ }
|
|
|
|
|
+ vo.setReasonDistribution(reasonStats);
|
|
|
|
|
+ vo.setMonthlyTrend(buildMonthlyReasonTrend(months, reasonStats,
|
|
|
|
|
+ tenantMonthMap.getOrDefault(tenantId, Collections.emptyMap())));
|
|
|
|
|
+ result.add(vo);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析退住原因统计区间:未传日期时默认近 12 个自然月
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param reqVO 请求参数
|
|
|
|
|
+ * @return [0] 起始日(含),[1] 结束日(含)
|
|
|
|
|
+ */
|
|
|
|
|
+ private LocalDate[] resolveRetreatReasonRange(OperationAnalysisRetreatReasonReqVO reqVO) {
|
|
|
|
|
+ LocalDate end = reqVO.getEndDate() != null ? reqVO.getEndDate() : LocalDate.now();
|
|
|
|
|
+ YearMonth endMonth = YearMonth.from(end);
|
|
|
|
|
+ LocalDate begin = reqVO.getStartDate() != null ? reqVO.getStartDate()
|
|
|
|
|
+ : endMonth.minusMonths(TREND_MONTHS - 1).atDay(1);
|
|
|
|
|
+ if (begin.isAfter(end)) {
|
|
|
|
|
+ begin = end;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (ChronoUnit.MONTHS.between(YearMonth.from(begin), endMonth) >= MAX_TREND_MONTHS) {
|
|
|
|
|
+ begin = endMonth.minusMonths(MAX_TREND_MONTHS - 1).atDay(1);
|
|
|
|
|
+ }
|
|
|
|
|
+ return new LocalDate[]{begin, end};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 归一化退住原因:空值归入「未填写」
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param reason 退住原因
|
|
|
|
|
+ * @return 展示用的退住原因
|
|
|
|
|
+ */
|
|
|
|
|
+ private static String normalizeReason(String reason) {
|
|
|
|
|
+ return StrUtil.isBlank(reason) ? UNFILLED_REASON : reason.trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 累加一条退住的入住时长:入住时间缺失或晚于退住日期时不计入平均值
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param agg 原因聚合器
|
|
|
|
|
+ * @param row 退住明细行
|
|
|
|
|
+ */
|
|
|
|
|
+ private void accumulateStay(ReasonAgg agg, RetreatReasonRowBO row) {
|
|
|
|
|
+ if (row.getCheckInTime() == null || row.getRetreatDate() == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ LocalDate checkInDate = row.getCheckInTime().toInstant()
|
|
|
|
|
+ .atZone(ZoneId.systemDefault())
|
|
|
|
|
+ .toLocalDate();
|
|
|
|
|
+ if (checkInDate.isAfter(row.getRetreatDate())) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ agg.stayDaySum += ChronoUnit.DAYS.between(checkInDate, row.getRetreatDate());
|
|
|
|
|
+ agg.stayMonthSum += ChronoUnit.MONTHS.between(checkInDate, row.getRetreatDate());
|
|
|
|
|
+ agg.staySampleCount++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构建退住原因分布:按人数降序,人数相同按原因名称升序
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param reasonMap 原因聚合
|
|
|
|
|
+ * @param total 该机构退住总人数
|
|
|
|
|
+ * @return 退住原因分布
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<ReasonStatVO> buildReasonStats(Map<String, ReasonAgg> reasonMap, long total) {
|
|
|
|
|
+ return reasonMap.entrySet().stream()
|
|
|
|
|
+ .sorted((o1, o2) -> {
|
|
|
|
|
+ int compare = Long.compare(o2.getValue().count, o1.getValue().count);
|
|
|
|
|
+ return compare != 0 ? compare : o1.getKey().compareTo(o2.getKey());
|
|
|
|
|
+ })
|
|
|
|
|
+ .map(entry -> {
|
|
|
|
|
+ ReasonAgg agg = entry.getValue();
|
|
|
|
|
+ ReasonStatVO vo = new ReasonStatVO();
|
|
|
|
|
+ vo.setReason(entry.getKey());
|
|
|
|
|
+ vo.setCount(agg.count);
|
|
|
|
|
+ vo.setRate(calcRate(agg.count, total));
|
|
|
|
|
+ vo.setAvgStayDays(calcAvg(agg.stayDaySum, agg.staySampleCount));
|
|
|
|
|
+ vo.setAvgStayMonths(calcAvg(agg.stayMonthSum, agg.staySampleCount));
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ })
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构建月度退住趋势:月份骨架来自统计区间,各原因无数据补 0
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param months 月份列表(升序)
|
|
|
|
|
+ * @param reasonStats 退住原因分布,决定趋势中原因的顺序
|
|
|
|
|
+ * @param monthMap 月份 -> 原因 -> 人数
|
|
|
|
|
+ * @return 月度退住趋势
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<MonthlyReasonTrendVO> buildMonthlyReasonTrend(List<String> months,
|
|
|
|
|
+ List<ReasonStatVO> reasonStats,
|
|
|
|
|
+ Map<String, Map<String, Long>> monthMap) {
|
|
|
|
|
+ List<MonthlyReasonTrendVO> result = new ArrayList<>(months.size());
|
|
|
|
|
+ for (String month : months) {
|
|
|
|
|
+ Map<String, Long> reasonCountMap = monthMap.getOrDefault(month, Collections.emptyMap());
|
|
|
|
|
+ MonthlyReasonTrendVO vo = new MonthlyReasonTrendVO();
|
|
|
|
|
+ vo.setMonth(month);
|
|
|
|
|
+ vo.setCount(reasonCountMap.values().stream().mapToLong(Long::longValue).sum());
|
|
|
|
|
+ List<ReasonCountVO> reasons = new ArrayList<>(reasonStats.size());
|
|
|
|
|
+ for (ReasonStatVO stat : reasonStats) {
|
|
|
|
|
+ ReasonCountVO item = new ReasonCountVO();
|
|
|
|
|
+ item.setReason(stat.getReason());
|
|
|
|
|
+ item.setCount(reasonCountMap.getOrDefault(stat.getReason(), 0L));
|
|
|
|
|
+ reasons.add(item);
|
|
|
|
|
+ }
|
|
|
|
|
+ vo.setReasons(reasons);
|
|
|
|
|
+ result.add(vo);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 长者结构聚合器
|
|
* 长者结构聚合器
|
|
|
*/
|
|
*/
|
|
@@ -983,4 +1181,31 @@ public class OperationAnalysisServiceImpl implements OperationAnalysisService {
|
|
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 退住原因聚合器
|
|
|
|
|
+ */
|
|
|
|
|
+ private static class ReasonAgg {
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 退住人数
|
|
|
|
|
+ */
|
|
|
|
|
+ private long count;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 入住天数合计
|
|
|
|
|
+ */
|
|
|
|
|
+ private long stayDaySum;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 入住月数合计
|
|
|
|
|
+ */
|
|
|
|
|
+ private long stayMonthSum;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 有效入住时长样本数(入住时间缺失或晚于退住日期的记为无效)
|
|
|
|
|
+ */
|
|
|
|
|
+ private long staySampleCount;
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
}
|
|
}
|