|
@@ -1,23 +1,28 @@
|
|
|
package cn.iocoder.yudao.module.system.service.biz;
|
|
package cn.iocoder.yudao.module.system.service.biz;
|
|
|
|
|
|
|
|
import cn.hutool.core.collection.CollUtil;
|
|
import cn.hutool.core.collection.CollUtil;
|
|
|
-import cn.hutool.core.util.ArrayUtil;
|
|
|
|
|
import cn.hutool.core.util.StrUtil;
|
|
import cn.hutool.core.util.StrUtil;
|
|
|
-import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
|
|
|
|
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
|
|
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
|
|
|
-import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
|
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisCheckTrendReqVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisCheckTrendRespVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisCheckTrendRespVO.CheckTrendItemVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisElderStructureReqVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisElderStructureRespVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisElderStructureRespVO.BucketVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisElderStructureRespVO.CampusStatVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisElderStructureRespVO.DictBucketVO;
|
|
|
|
|
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisElderStructureRespVO.NurseLevelBucketVO;
|
|
|
import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisOverviewDynamicsReqVO;
|
|
import cn.iocoder.yudao.module.system.controller.admin.biz.vo.operationanalysis.OperationAnalysisOverviewDynamicsReqVO;
|
|
|
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.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.ElderStructureRowBO;
|
|
|
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.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.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;
|
|
@@ -28,18 +33,20 @@ import java.math.RoundingMode;
|
|
|
import java.time.DayOfWeek;
|
|
import java.time.DayOfWeek;
|
|
|
import java.time.LocalDate;
|
|
import java.time.LocalDate;
|
|
|
import java.time.LocalDateTime;
|
|
import java.time.LocalDateTime;
|
|
|
|
|
+import java.time.Period;
|
|
|
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.ChronoUnit;
|
|
|
import java.time.temporal.TemporalAdjusters;
|
|
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;
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
-import java.util.Objects;
|
|
|
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -79,12 +86,51 @@ public class OperationAnalysisServiceImpl implements OperationAnalysisService {
|
|
|
private static final String UNRATED_NURSE_LEVEL_NAME = "未定级";
|
|
private static final String UNRATED_NURSE_LEVEL_NAME = "未定级";
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 集团账号的租户ID,集团账号需要展开为旗下所有机构
|
|
|
|
|
|
|
+ * 分组值为空时展示的名称
|
|
|
*/
|
|
*/
|
|
|
- private static final Long GROUP_TENANT_ID = 1L;
|
|
|
|
|
|
|
+ private static final String UNSET_LABEL = "未设置";
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按日统计时最多返回的天数
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final int MAX_TREND_DAYS = 366;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按日统计时默认返回的天数
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final int DEFAULT_TREND_DAYS = 30;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按月统计时最多返回的月份数
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final int MAX_TREND_MONTHS = 120;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 护理等级分布最多返回的等级数量
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final int MAX_NURSE_LEVEL_BUCKETS = 10;
|
|
|
|
|
|
|
|
private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
|
|
private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
|
|
|
|
|
|
|
|
|
|
+ private static final DateTimeFormatter DAY_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 性别分段骨架
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final List<String> SEX_BUCKET_LABELS = buildLabels("男", "女");
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 年龄段分段骨架
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final List<String> AGE_BUCKET_LABELS =
|
|
|
|
|
+ buildLabels("<60", "60-69", "70-79", "80-89", "90-99", "≥100");
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 入住时长分段骨架
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final List<String> STAY_BUCKET_LABELS =
|
|
|
|
|
+ buildLabels("<1年", "1-3年", "3-5年", "5-10年", "≥10年");
|
|
|
|
|
+
|
|
|
@Resource
|
|
@Resource
|
|
|
private OperationAnalysisMapper operationAnalysisMapper;
|
|
private OperationAnalysisMapper operationAnalysisMapper;
|
|
|
|
|
|
|
@@ -92,12 +138,12 @@ public class OperationAnalysisServiceImpl implements OperationAnalysisService {
|
|
|
private TenantService tenantService;
|
|
private TenantService tenantService;
|
|
|
|
|
|
|
|
@Resource
|
|
@Resource
|
|
|
- private GroupTenantMapper groupTenantMapper;
|
|
|
|
|
|
|
+ private TenantScopeService tenantScopeService;
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
@TenantIgnore
|
|
@TenantIgnore
|
|
|
public List<OperationAnalysisOverviewDynamicsRespVO> getOverviewDynamics(OperationAnalysisOverviewDynamicsReqVO reqVO) {
|
|
public List<OperationAnalysisOverviewDynamicsRespVO> getOverviewDynamics(OperationAnalysisOverviewDynamicsReqVO reqVO) {
|
|
|
- List<Long> tenantIds = resolveTenantIds(reqVO.getTenantIds());
|
|
|
|
|
|
|
+ List<Long> tenantIds = tenantScopeService.resolveTenantIds(reqVO.getTenantIds());
|
|
|
if (CollUtil.isEmpty(tenantIds)) {
|
|
if (CollUtil.isEmpty(tenantIds)) {
|
|
|
return Collections.emptyList();
|
|
return Collections.emptyList();
|
|
|
}
|
|
}
|
|
@@ -259,35 +305,6 @@ public class OperationAnalysisServiceImpl implements OperationAnalysisService {
|
|
|
.toPlainString() + "%";
|
|
.toPlainString() + "%";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 解析要统计的机构ID:优先使用入参,否则取当前登录机构;集团账号展开为旗下所有机构
|
|
|
|
|
- *
|
|
|
|
|
- * @param tenantIds 入参机构ID数组
|
|
|
|
|
- * @return 机构ID列表
|
|
|
|
|
- */
|
|
|
|
|
- private List<Long> resolveTenantIds(Long[] tenantIds) {
|
|
|
|
|
- if (ArrayUtil.isNotEmpty(tenantIds)) {
|
|
|
|
|
- return Arrays.stream(tenantIds)
|
|
|
|
|
- .filter(Objects::nonNull)
|
|
|
|
|
- .distinct()
|
|
|
|
|
- .collect(Collectors.toList());
|
|
|
|
|
- }
|
|
|
|
|
- Long currentTenantId = TenantContextHolder.getTenantId();
|
|
|
|
|
- if (currentTenantId == null) {
|
|
|
|
|
- return Collections.emptyList();
|
|
|
|
|
- }
|
|
|
|
|
- if (GROUP_TENANT_ID.equals(currentTenantId)) {
|
|
|
|
|
- return groupTenantMapper.selectList(new LambdaQueryWrapperX<GroupTenantDO>()
|
|
|
|
|
- .eq(GroupTenantDO::getDeleted, 0))
|
|
|
|
|
- .stream()
|
|
|
|
|
- .map(GroupTenantDO::getOrgTenantId)
|
|
|
|
|
- .filter(Objects::nonNull)
|
|
|
|
|
- .distinct()
|
|
|
|
|
- .collect(Collectors.toList());
|
|
|
|
|
- }
|
|
|
|
|
- return Collections.singletonList(currentTenantId);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* 构建近 12 个月已完成退住趋势,无数据的月份补 0
|
|
* 构建近 12 个月已完成退住趋势,无数据的月份补 0
|
|
|
*
|
|
*
|
|
@@ -371,4 +388,599 @@ public class OperationAnalysisServiceImpl implements OperationAnalysisService {
|
|
|
return value == null ? 0L : value;
|
|
return value == null ? 0L : value;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private static Date toDate(LocalDate date) {
|
|
|
|
|
+ return Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构造分段骨架:固定分段 + 「未设置」,保证各分段人数之和 = 总数
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param labels 固定分段名称
|
|
|
|
|
+ * @return 分段名称列表
|
|
|
|
|
+ */
|
|
|
|
|
+ private static List<String> buildLabels(String... labels) {
|
|
|
|
|
+ List<String> result = new ArrayList<>(Arrays.asList(labels));
|
|
|
|
|
+ result.add(UNSET_LABEL);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 入住 / 退住趋势 ==========
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @TenantIgnore
|
|
|
|
|
+ public OperationAnalysisCheckTrendRespVO getCheckTrend(OperationAnalysisCheckTrendReqVO reqVO) {
|
|
|
|
|
+ OperationAnalysisCheckTrendRespVO respVO = new OperationAnalysisCheckTrendRespVO();
|
|
|
|
|
+ respVO.setItems(new ArrayList<>());
|
|
|
|
|
+ List<Long> tenantIds = tenantScopeService.resolveTenantIds(reqVO.getTenantIds());
|
|
|
|
|
+ if (CollUtil.isEmpty(tenantIds)) {
|
|
|
|
|
+ return respVO;
|
|
|
|
|
+ }
|
|
|
|
|
+ boolean byDay = "day".equalsIgnoreCase(reqVO.getGranularity());
|
|
|
|
|
+ LocalDate[] range = resolveTrendRange(reqVO, byDay);
|
|
|
|
|
+ LocalDate begin = range[0];
|
|
|
|
|
+ LocalDate end = range[1];
|
|
|
|
|
+ int orgType = reqVO.getOrgType() == null ? 1 : reqVO.getOrgType();
|
|
|
|
|
+
|
|
|
|
|
+ // 按天取数,Java 侧再按月 / 按日归集,避免 DATE_FORMAT 传参带来的方言与注入问题
|
|
|
|
|
+ Map<String, Long> checkInMap = toDayCountMap(
|
|
|
|
|
+ operationAnalysisMapper.selectCheckInDailyTrend(tenantIds, orgType, begin, end.plusDays(1)));
|
|
|
|
|
+ Map<String, Long> checkOutMap = toDayCountMap(
|
|
|
|
|
+ operationAnalysisMapper.selectCheckOutDailyTrend(tenantIds, orgType, begin, end.plusDays(1)));
|
|
|
|
|
+ long checkInBefore = nullToZero(operationAnalysisMapper.selectCheckInCountBefore(tenantIds, orgType, begin));
|
|
|
|
|
+ long checkOutBefore = nullToZero(operationAnalysisMapper.selectCheckOutCountBefore(tenantIds, orgType, begin));
|
|
|
|
|
+ long totalBedCount = operationAnalysisMapper.selectBedStructure(tenantIds)
|
|
|
|
|
+ .stream()
|
|
|
|
|
+ .mapToLong(bo -> nullToZero(bo.getTotalBedCount()))
|
|
|
|
|
+ .sum();
|
|
|
|
|
+
|
|
|
|
|
+ // 期末入住率依赖历史床位数快照,当前无快照表,故仅当前期间(含)之后返回数值,历史期间返回 null
|
|
|
|
|
+ String currentPeriod = byDay ? LocalDate.now().format(DAY_FORMATTER)
|
|
|
|
|
+ : YearMonth.now().format(MONTH_FORMATTER);
|
|
|
|
|
+
|
|
|
|
|
+ long inHospital = checkInBefore - checkOutBefore;
|
|
|
|
|
+ List<String> periods = buildPeriods(begin, end, byDay);
|
|
|
|
|
+ List<CheckTrendItemVO> items = new ArrayList<>(periods.size());
|
|
|
|
|
+ for (String period : periods) {
|
|
|
|
|
+ long checkIn = sumPeriod(checkInMap, period, byDay);
|
|
|
|
|
+ long checkOut = sumPeriod(checkOutMap, period, byDay);
|
|
|
|
|
+ CheckTrendItemVO item = new OperationAnalysisCheckTrendRespVO.CheckTrendItemVO();
|
|
|
|
|
+ item.setPeriod(period);
|
|
|
|
|
+ item.setBeginInHospital(inHospital);
|
|
|
|
|
+ item.setCheckInCount(checkIn);
|
|
|
|
|
+ item.setCheckOutCount(checkOut);
|
|
|
|
|
+ item.setNetGrowth(checkIn - checkOut);
|
|
|
|
|
+ // 恒满足 期初 + 入住 - 退住 = 期末
|
|
|
|
|
+ inHospital = inHospital + checkIn - checkOut;
|
|
|
|
|
+ item.setEndInHospital(inHospital);
|
|
|
|
|
+ item.setEndOccupancyRate(period.compareTo(currentPeriod) >= 0
|
|
|
|
|
+ ? calcRate(inHospital, totalBedCount) : null);
|
|
|
|
|
+ items.add(item);
|
|
|
|
|
+ }
|
|
|
|
|
+ respVO.setItems(items);
|
|
|
|
|
+ return respVO;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析趋势统计区间
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param reqVO 请求参数
|
|
|
|
|
+ * @param byDay 是否按日
|
|
|
|
|
+ * @return [0] 起始日(含),[1] 结束日(含)
|
|
|
|
|
+ */
|
|
|
|
|
+ private LocalDate[] resolveTrendRange(OperationAnalysisCheckTrendReqVO reqVO, boolean byDay) {
|
|
|
|
|
+ if (byDay) {
|
|
|
|
|
+ LocalDate end = reqVO.getEndDate() != null ? reqVO.getEndDate() : LocalDate.now();
|
|
|
|
|
+ LocalDate begin = reqVO.getStartDate() != null ? reqVO.getStartDate()
|
|
|
|
|
+ : end.minusDays(DEFAULT_TREND_DAYS - 1);
|
|
|
|
|
+ if (begin.isAfter(end)) {
|
|
|
|
|
+ begin = end;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (ChronoUnit.DAYS.between(begin, end) >= MAX_TREND_DAYS) {
|
|
|
|
|
+ begin = end.minusDays(MAX_TREND_DAYS - 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ return new LocalDate[]{begin, end};
|
|
|
|
|
+ }
|
|
|
|
|
+ YearMonth endMonth = reqVO.getEndDate() != null ? YearMonth.from(reqVO.getEndDate()) : YearMonth.now();
|
|
|
|
|
+ YearMonth beginMonth;
|
|
|
|
|
+ if (reqVO.getStartDate() != null) {
|
|
|
|
|
+ beginMonth = YearMonth.from(reqVO.getStartDate());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ int months = (reqVO.getMonths() == null || reqVO.getMonths() <= 0)
|
|
|
|
|
+ ? TREND_MONTHS : Math.min(reqVO.getMonths(), MAX_TREND_MONTHS);
|
|
|
|
|
+ beginMonth = endMonth.minusMonths(months - 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (beginMonth.isAfter(endMonth)) {
|
|
|
|
|
+ beginMonth = endMonth;
|
|
|
|
|
+ }
|
|
|
|
|
+ return new LocalDate[]{beginMonth.atDay(1), endMonth.atEndOfMonth()};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 生成期间列表(升序)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param begin 起始日(含)
|
|
|
|
|
+ * @param end 结束日(含)
|
|
|
|
|
+ * @param byDay 是否按日
|
|
|
|
|
+ * @return 期间列表
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<String> buildPeriods(LocalDate begin, LocalDate end, boolean byDay) {
|
|
|
|
|
+ List<String> periods = new ArrayList<>();
|
|
|
|
|
+ if (byDay) {
|
|
|
|
|
+ for (LocalDate date = begin; !date.isAfter(end); date = date.plusDays(1)) {
|
|
|
|
|
+ periods.add(date.format(DAY_FORMATTER));
|
|
|
|
|
+ }
|
|
|
|
|
+ return periods;
|
|
|
|
|
+ }
|
|
|
|
|
+ YearMonth endMonth = YearMonth.from(end);
|
|
|
|
|
+ for (YearMonth month = YearMonth.from(begin); !month.isAfter(endMonth); month = month.plusMonths(1)) {
|
|
|
|
|
+ periods.add(month.format(MONTH_FORMATTER));
|
|
|
|
|
+ }
|
|
|
|
|
+ return periods;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按天统计结果转成「日期 -> 数量」
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param list 按天统计结果
|
|
|
|
|
+ * @return 日期统计
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<String, Long> toDayCountMap(List<OperationTenantMonthCountBO> list) {
|
|
|
|
|
+ Map<String, Long> map = new HashMap<>();
|
|
|
|
|
+ if (CollUtil.isEmpty(list)) {
|
|
|
|
|
+ return map;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (OperationTenantMonthCountBO bo : list) {
|
|
|
|
|
+ if (StrUtil.isBlank(bo.getMonth())) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ map.merge(bo.getMonth(), nullToZero(bo.getCount()), Long::sum);
|
|
|
|
|
+ }
|
|
|
|
|
+ return map;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 汇总某个期间的入住 / 退住数量
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param map 按天统计
|
|
|
|
|
+ * @param period 期间
|
|
|
|
|
+ * @param byDay 是否按日
|
|
|
|
|
+ * @return 期间数量
|
|
|
|
|
+ */
|
|
|
|
|
+ private long sumPeriod(Map<String, Long> map, String period, boolean byDay) {
|
|
|
|
|
+ if (byDay) {
|
|
|
|
|
+ return map.getOrDefault(period, 0L);
|
|
|
|
|
+ }
|
|
|
|
|
+ long sum = 0;
|
|
|
|
|
+ for (Map.Entry<String, Long> entry : map.entrySet()) {
|
|
|
|
|
+ if (entry.getKey().startsWith(period)) {
|
|
|
|
|
+ sum += entry.getValue();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return sum;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 计算比率(保留 1 位小数)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param numerator 分子
|
|
|
|
|
+ * @param denominator 分母
|
|
|
|
|
+ * @return 百分比,分母为 0 时返回 null
|
|
|
|
|
+ */
|
|
|
|
|
+ private static BigDecimal calcRate(long numerator, long denominator) {
|
|
|
|
|
+ if (denominator <= 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return BigDecimal.valueOf(numerator)
|
|
|
|
|
+ .multiply(BigDecimal.valueOf(100))
|
|
|
|
|
+ .divide(BigDecimal.valueOf(denominator), 1, RoundingMode.HALF_UP);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 长者结构聚合 ==========
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @TenantIgnore
|
|
|
|
|
+ public OperationAnalysisElderStructureRespVO getElderStructure(OperationAnalysisElderStructureReqVO reqVO) {
|
|
|
|
|
+ List<Long> tenantIds = tenantScopeService.resolveTenantIds(reqVO.getTenantIds());
|
|
|
|
|
+ int orgType = reqVO.getOrgType() == null ? 1 : reqVO.getOrgType();
|
|
|
|
|
+ List<ElderStructureRowBO> rows = CollUtil.isEmpty(tenantIds) ? Collections.emptyList()
|
|
|
|
|
+ : operationAnalysisMapper.selectElderStructureRows(tenantIds, orgType);
|
|
|
|
|
+
|
|
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
|
|
+ StructureAgg totalAgg = new StructureAgg();
|
|
|
|
|
+ Map<Long, StructureAgg> campusAggMap = new LinkedHashMap<>();
|
|
|
|
|
+ for (Long tenantId : tenantIds) {
|
|
|
|
|
+ campusAggMap.put(tenantId, new StructureAgg());
|
|
|
|
|
+ }
|
|
|
|
|
+ for (ElderStructureRowBO row : rows) {
|
|
|
|
|
+ StructureAgg campusAgg = campusAggMap.computeIfAbsent(row.getTenantId(), k -> new StructureAgg());
|
|
|
|
|
+ fillAgg(totalAgg, row, today);
|
|
|
|
|
+ fillAgg(campusAgg, row, today);
|
|
|
|
|
+ }
|
|
|
|
|
+ return buildElderStructureRespVO(totalAgg, campusAggMap);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 把一条在住长者明细累加进聚合器
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param agg 聚合器
|
|
|
|
|
+ * @param row 明细行
|
|
|
|
|
+ * @param today 当前日期
|
|
|
|
|
+ */
|
|
|
|
|
+ private void fillAgg(StructureAgg agg, ElderStructureRowBO row, LocalDate today) {
|
|
|
|
|
+ agg.total++;
|
|
|
|
|
+ Integer sex = row.getElderSex();
|
|
|
|
|
+ if (sex != null && sex == 1) {
|
|
|
|
|
+ agg.male++;
|
|
|
|
|
+ } else if (sex != null && sex == 2) {
|
|
|
|
|
+ agg.female++;
|
|
|
|
|
+ }
|
|
|
|
|
+ agg.sexBuckets.merge(resolveSexLabel(sex), 1L, Long::sum);
|
|
|
|
|
+
|
|
|
|
|
+ Integer age = resolveAge(row, today);
|
|
|
|
|
+ if (age != null) {
|
|
|
|
|
+ agg.ageSum += age;
|
|
|
|
|
+ agg.ageCount++;
|
|
|
|
|
+ if (age >= 80) {
|
|
|
|
|
+ agg.over80++;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (age >= 90) {
|
|
|
|
|
+ agg.over90++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ agg.ageBuckets.merge(resolveAgeBucket(age), 1L, Long::sum);
|
|
|
|
|
+
|
|
|
|
|
+ Long stayMonths = resolveStayMonths(row, today);
|
|
|
|
|
+ if (stayMonths != null) {
|
|
|
|
|
+ agg.staySum += stayMonths;
|
|
|
|
|
+ agg.stayCount++;
|
|
|
|
|
+ }
|
|
|
|
|
+ agg.stayBuckets.merge(resolveStayBucket(stayMonths), 1L, Long::sum);
|
|
|
|
|
+
|
|
|
|
|
+ Long nurseLevelId = row.getNurseLevelId();
|
|
|
|
|
+ String nurseLevelName = StrUtil.isNotBlank(row.getNurseLevelName())
|
|
|
|
|
+ ? row.getNurseLevelName() : UNRATED_NURSE_LEVEL_NAME;
|
|
|
|
|
+ agg.nurseBuckets.computeIfAbsent(nurseLevelId, k -> new NurseCounter(nurseLevelName)).count++;
|
|
|
|
|
+ agg.educationBuckets.merge(row.getDegreeEducation(), 1L, Long::sum);
|
|
|
|
|
+ agg.paymentBuckets.merge(row.getPaymentMethod(), 1L, Long::sum);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 组装长者结构响应
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param totalAgg 汇总聚合器
|
|
|
|
|
+ * @param campusAggMap 各院区聚合器
|
|
|
|
|
+ * @return 长者结构聚合结果
|
|
|
|
|
+ */
|
|
|
|
|
+ private OperationAnalysisElderStructureRespVO buildElderStructureRespVO(StructureAgg totalAgg,
|
|
|
|
|
+ Map<Long, StructureAgg> campusAggMap) {
|
|
|
|
|
+ OperationAnalysisElderStructureRespVO respVO = new OperationAnalysisElderStructureRespVO();
|
|
|
|
|
+ respVO.setTotal(totalAgg.total);
|
|
|
|
|
+ respVO.setMaleCount(totalAgg.male);
|
|
|
|
|
+ respVO.setFemaleCount(totalAgg.female);
|
|
|
|
|
+ respVO.setAvgAge(calcAvg(totalAgg.ageSum, totalAgg.ageCount));
|
|
|
|
|
+ respVO.setAvgStayMonths(calcAvg(totalAgg.staySum, totalAgg.stayCount));
|
|
|
|
|
+ respVO.setOver80Count(totalAgg.over80);
|
|
|
|
|
+ respVO.setOver90Count(totalAgg.over90);
|
|
|
|
|
+ respVO.setSexBuckets(toBucketVOList(SEX_BUCKET_LABELS, totalAgg.sexBuckets));
|
|
|
|
|
+ respVO.setAgeBuckets(toBucketVOList(AGE_BUCKET_LABELS, totalAgg.ageBuckets));
|
|
|
|
|
+ respVO.setStayBuckets(toBucketVOList(STAY_BUCKET_LABELS, totalAgg.stayBuckets));
|
|
|
|
|
+ respVO.setNurseLevelBuckets(toNurseLevelBucketList(totalAgg.nurseBuckets));
|
|
|
|
|
+ respVO.setEducationBuckets(toDictBucketList(totalAgg.educationBuckets));
|
|
|
|
|
+ respVO.setPaymentBuckets(toDictBucketList(totalAgg.paymentBuckets));
|
|
|
|
|
+ respVO.setCampusStats(toCampusStatList(campusAggMap));
|
|
|
|
|
+ return respVO;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析长者年龄:优先取档案年龄,为空时用出生日期兜底
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param row 明细行
|
|
|
|
|
+ * @param today 当前日期
|
|
|
|
|
+ * @return 年龄,为空表示未知
|
|
|
|
|
+ */
|
|
|
|
|
+ private Integer resolveAge(ElderStructureRowBO row, LocalDate today) {
|
|
|
|
|
+ if (row.getElderAge() != null) {
|
|
|
|
|
+ return row.getElderAge();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (row.getBirthday() == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ LocalDate birthday = row.getBirthday().toInstant()
|
|
|
|
|
+ .atZone(ZoneId.systemDefault())
|
|
|
|
|
+ .toLocalDate();
|
|
|
|
|
+ if (birthday.isAfter(today)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return Period.between(birthday, today).getYears();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析入住月数
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param row 明细行
|
|
|
|
|
+ * @param today 当前日期
|
|
|
|
|
+ * @return 入住月数,为空表示未知
|
|
|
|
|
+ */
|
|
|
|
|
+ private Long resolveStayMonths(ElderStructureRowBO row, LocalDate today) {
|
|
|
|
|
+ if (row.getCheckInTime() == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ LocalDate checkInDate = row.getCheckInTime().toInstant()
|
|
|
|
|
+ .atZone(ZoneId.systemDefault())
|
|
|
|
|
+ .toLocalDate();
|
|
|
|
|
+ if (checkInDate.isAfter(today)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return ChronoUnit.MONTHS.between(checkInDate, today);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String resolveSexLabel(Integer sex) {
|
|
|
|
|
+ if (sex == null) {
|
|
|
|
|
+ return UNSET_LABEL;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sex == 1) {
|
|
|
|
|
+ return "男";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sex == 2) {
|
|
|
|
|
+ return "女";
|
|
|
|
|
+ }
|
|
|
|
|
+ return UNSET_LABEL;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String resolveAgeBucket(Integer age) {
|
|
|
|
|
+ if (age == null) {
|
|
|
|
|
+ return UNSET_LABEL;
|
|
|
|
|
+ }
|
|
|
|
|
+ int value = Math.max(age, 0);
|
|
|
|
|
+ if (value < 60) {
|
|
|
|
|
+ return "<60";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value <= 69) {
|
|
|
|
|
+ return "60-69";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value <= 79) {
|
|
|
|
|
+ return "70-79";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value <= 89) {
|
|
|
|
|
+ return "80-89";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value <= 99) {
|
|
|
|
|
+ return "90-99";
|
|
|
|
|
+ }
|
|
|
|
|
+ return "≥100";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String resolveStayBucket(Long stayMonths) {
|
|
|
|
|
+ if (stayMonths == null) {
|
|
|
|
|
+ return UNSET_LABEL;
|
|
|
|
|
+ }
|
|
|
|
|
+ long value = Math.max(stayMonths, 0L);
|
|
|
|
|
+ if (value < 12) {
|
|
|
|
|
+ return "<1年";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value < 36) {
|
|
|
|
|
+ return "1-3年";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value < 60) {
|
|
|
|
|
+ return "3-5年";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (value < 120) {
|
|
|
|
|
+ return "5-10年";
|
|
|
|
|
+ }
|
|
|
|
|
+ return "≥10年";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按固定骨架输出分段,缺失分段补 0
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param labels 分段骨架
|
|
|
|
|
+ * @param counts 实际统计
|
|
|
|
|
+ * @return 分段列表
|
|
|
|
|
+ */
|
|
|
|
|
+ private static List<BucketVO> toBucketVOList(List<String> labels, Map<String, Long> counts) {
|
|
|
|
|
+ List<BucketVO> result = new ArrayList<>(labels.size());
|
|
|
|
|
+ for (String label : labels) {
|
|
|
|
|
+ BucketVO vo = new BucketVO();
|
|
|
|
|
+ vo.setLabel(label);
|
|
|
|
|
+ vo.setCount(counts.getOrDefault(label, 0L));
|
|
|
|
|
+ result.add(vo);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 护理等级分布:按人数降序取 TOP N
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param counters 护理等级统计
|
|
|
|
|
+ * @return 护理等级分布
|
|
|
|
|
+ */
|
|
|
|
|
+ private static List<NurseLevelBucketVO> toNurseLevelBucketList(Map<Long, NurseCounter> counters) {
|
|
|
|
|
+ return counters.entrySet().stream()
|
|
|
|
|
+ .sorted((o1, o2) -> {
|
|
|
|
|
+ int compare = Long.compare(o2.getValue().count, o1.getValue().count);
|
|
|
|
|
+ return compare != 0 ? compare : o1.getValue().name.compareTo(o2.getValue().name);
|
|
|
|
|
+ })
|
|
|
|
|
+ .limit(MAX_NURSE_LEVEL_BUCKETS)
|
|
|
|
|
+ .map(entry -> {
|
|
|
|
|
+ NurseLevelBucketVO vo = new NurseLevelBucketVO();
|
|
|
|
|
+ vo.setNurseLevelId(entry.getKey());
|
|
|
|
|
+ vo.setNurseLevelName(entry.getValue().name);
|
|
|
|
|
+ vo.setCount(entry.getValue().count);
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ })
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 字典分段:value 返回字典原值,由前端翻译
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param counts 字典值统计
|
|
|
|
|
+ * @return 字典分段列表
|
|
|
|
|
+ */
|
|
|
|
|
+ private static List<DictBucketVO> toDictBucketList(Map<Integer, Long> counts) {
|
|
|
|
|
+ return counts.entrySet().stream()
|
|
|
|
|
+ .sorted((o1, o2) -> Long.compare(o2.getValue(), o1.getValue()))
|
|
|
|
|
+ .map(entry -> {
|
|
|
|
|
+ DictBucketVO vo = new DictBucketVO();
|
|
|
|
|
+ vo.setValue(entry.getKey());
|
|
|
|
|
+ vo.setLabel(String.valueOf(entry.getKey()));
|
|
|
|
|
+ vo.setCount(entry.getValue());
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ })
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 各院区明细
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param campusAggMap 各院区聚合器
|
|
|
|
|
+ * @return 院区明细列表
|
|
|
|
|
+ */
|
|
|
|
|
+ private List<CampusStatVO> toCampusStatList(Map<Long, StructureAgg> campusAggMap) {
|
|
|
|
|
+ List<CampusStatVO> result = new ArrayList<>(campusAggMap.size());
|
|
|
|
|
+ for (Map.Entry<Long, StructureAgg> entry : campusAggMap.entrySet()) {
|
|
|
|
|
+ StructureAgg agg = entry.getValue();
|
|
|
|
|
+ CampusStatVO vo = new CampusStatVO();
|
|
|
|
|
+ vo.setTenantId(entry.getKey());
|
|
|
|
|
+ vo.setTenantName(getTenantName(entry.getKey()));
|
|
|
|
|
+ vo.setTotal(agg.total);
|
|
|
|
|
+ vo.setMaleCount(agg.male);
|
|
|
|
|
+ vo.setFemaleCount(agg.female);
|
|
|
|
|
+ vo.setAvgAge(calcAvg(agg.ageSum, agg.ageCount));
|
|
|
|
|
+ vo.setAvgStayMonths(calcAvg(agg.staySum, agg.stayCount));
|
|
|
|
|
+ vo.setOver80Count(agg.over80);
|
|
|
|
|
+ vo.setMainNurseLevelName(findMainNurseLevelName(agg.nurseBuckets));
|
|
|
|
|
+ result.add(vo);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 取人数最多的护理等级名称
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param counters 护理等级统计
|
|
|
|
|
+ * @return 主要护理等级名称,无数据时返回 null
|
|
|
|
|
+ */
|
|
|
|
|
+ private static String findMainNurseLevelName(Map<Long, NurseCounter> counters) {
|
|
|
|
|
+ NurseCounter main = null;
|
|
|
|
|
+ for (NurseCounter counter : counters.values()) {
|
|
|
|
|
+ if (main == null || counter.count > main.count) {
|
|
|
|
|
+ main = counter;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return main == null ? null : main.name;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 计算平均值(保留 1 位小数)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param sum 合计
|
|
|
|
|
+ * @param count 数量
|
|
|
|
|
+ * @return 平均值,数量为 0 时返回 null
|
|
|
|
|
+ */
|
|
|
|
|
+ private static BigDecimal calcAvg(long sum, long count) {
|
|
|
|
|
+ if (count <= 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return BigDecimal.valueOf(sum)
|
|
|
|
|
+ .divide(BigDecimal.valueOf(count), 1, RoundingMode.HALF_UP);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 长者结构聚合器
|
|
|
|
|
+ */
|
|
|
|
|
+ private static class StructureAgg {
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 总数
|
|
|
|
|
+ */
|
|
|
|
|
+ private long total;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 男性数
|
|
|
|
|
+ */
|
|
|
|
|
+ private long male;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 女性数
|
|
|
|
|
+ */
|
|
|
|
|
+ private long female;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 80 岁以上人数
|
|
|
|
|
+ */
|
|
|
|
|
+ private long over80;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 90 岁以上人数
|
|
|
|
|
+ */
|
|
|
|
|
+ private long over90;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 年龄合计 / 有效年龄数
|
|
|
|
|
+ */
|
|
|
|
|
+ private long ageSum;
|
|
|
|
|
+
|
|
|
|
|
+ private long ageCount;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 入住月数合计 / 有效入住数
|
|
|
|
|
+ */
|
|
|
|
|
+ private long staySum;
|
|
|
|
|
+
|
|
|
|
|
+ private long stayCount;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 性别分段
|
|
|
|
|
+ */
|
|
|
|
|
+ private final Map<String, Long> sexBuckets = new HashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 年龄段分段
|
|
|
|
|
+ */
|
|
|
|
|
+ private final Map<String, Long> ageBuckets = new HashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 入住时长分段
|
|
|
|
|
+ */
|
|
|
|
|
+ private final Map<String, Long> stayBuckets = new HashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 护理等级分段
|
|
|
|
|
+ */
|
|
|
|
|
+ private final Map<Long, NurseCounter> nurseBuckets = new LinkedHashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 文化程度分段
|
|
|
|
|
+ */
|
|
|
|
|
+ private final Map<Integer, Long> educationBuckets = new HashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 缴费方式分段
|
|
|
|
|
+ */
|
|
|
|
|
+ private final Map<Integer, Long> paymentBuckets = new HashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 护理等级计数器
|
|
|
|
|
+ */
|
|
|
|
|
+ private static class NurseCounter {
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 护理等级名称
|
|
|
|
|
+ */
|
|
|
|
|
+ private final String name;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 人数
|
|
|
|
|
+ */
|
|
|
|
|
+ private long count;
|
|
|
|
|
+
|
|
|
|
|
+ NurseCounter(String name) {
|
|
|
|
|
+ this.name = name;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
}
|
|
}
|