Переглянути джерело

新增
1、长者档案导出增加供养类型
2、长者档案列表增加供养类型筛选条件
3、消费券列表增加床位号显示
BUGFIX
1、解决退住申请校验退住月账单时没有过滤零星账单的问题。
2、解决消费券明细中部分长者床位号显示不正确问题
3、解决长者档案中供养人无法删除问题
4、解决账单导出没有优先按照项目名称进行优先匹配的问题
5、解决集团跳转院区后无法查看报表的问题
6、解决应收报表离住长者在非离住月和返院月缺少记录的问题
7、解决应收报表在部分长者标准金额显示异常问题

liangwenxuan 1 тиждень тому
батько
коміт
effa390ddf

+ 1 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/bpm/BpmElderlyExpenseApiImpl.java

@@ -437,6 +437,7 @@ public class BpmElderlyExpenseApiImpl implements BpmElderlyExpenseApi {
             List<ExpenseOrderDO> expenseOrderDOS = expenseOrderMapper.selectList(new LambdaQueryWrapperX<ExpenseOrderDO>()
                     .eq(ExpenseOrderDO::getElderId, elderId)
                     .eq(ExpenseOrderDO::getBillingMonth, deadLineYearMonth)
+                    .in(ExpenseOrderDO::getType,1,2)
             );
             if(CollectionUtil.isEmpty(expenseOrderDOS)){
                 throw exception(APPLY_RETREAT_CURRENT_EXPENSE_ORDER_NOT_EXISTS);

+ 253 - 46
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/order/OrderApiImpl.java

@@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
 import cn.iocoder.yudao.module.system.dal.mysql.biz.*;
 import cn.iocoder.yudao.module.system.dal.mysql.tenant.TenantMapper;
 import cn.iocoder.yudao.module.system.enums.change.BusinessConstants;
+import cn.iocoder.yudao.module.system.service.biz.BuildService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -22,6 +23,7 @@ import javax.annotation.Resource;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.time.YearMonth;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
@@ -95,6 +97,10 @@ public class OrderApiImpl implements OrderApi {
     private ElderlyInfoMapper elderlyInfoMapper;
     @Resource
     private ElderlyAskLeaveMapper elderlyAskLeaveMapper;
+    @Resource
+    private ElderlyTempOutMapper elderlyTempOutMapper;
+    @Resource
+    private BuildService buildService;
 
     @Resource
     private OutboundRefundConfigMapper outboundRefundConfigMapper;
@@ -274,34 +280,34 @@ public class OrderApiImpl implements OrderApi {
 
             BigDecimal billedStandardAmount = standardAmount;
 
-            if (billYm != null) {
-                int daysInBillMonth = billYm.lengthOfMonth();
-                LocalDate changeStartDate = expenseItemDO.getChangeStartDate();
-                LocalDate changeEndDate = expenseItemDO.getChangeEndDate();
-
-                // 费用生效区间与账单月做交集:
-                // - 无开始日:视为本月月初
-                // - 无结束日:视为本月月末
-                LocalDate periodStart = changeStartDate == null ? billStart : changeStartDate;
-                LocalDate periodEnd = changeEndDate == null ? billEnd : changeEndDate;
-
-                LocalDate overlapStart = periodStart.isAfter(billStart) ? periodStart : billStart;
-                LocalDate overlapEnd = periodEnd.isBefore(billEnd) ? periodEnd : billEnd;
-
-                if (overlapEnd.isBefore(overlapStart)) {
-                    // 该费用在账单月无覆盖,跳过
-                    continue;
-                }
-
-                long actualDays = ChronoUnit.DAYS.between(overlapStart, overlapEnd) + 1;
-                if (daysInBillMonth > 0 && actualDays > 0 && actualDays != daysInBillMonth) {
-                    BigDecimal dayStandard = standardAmount
-                            .divide(BigDecimal.valueOf(daysInBillMonth), 8, RoundingMode.HALF_UP);
-                    billedStandardAmount = dayStandard
-                            .multiply(BigDecimal.valueOf(actualDays))
-                            .setScale(2, RoundingMode.HALF_UP);
-                }
-            }
+//            if (billYm != null) {
+//                int daysInBillMonth = billYm.lengthOfMonth();
+//                LocalDate changeStartDate = expenseItemDO.getChangeStartDate();
+//                LocalDate changeEndDate = expenseItemDO.getChangeEndDate();
+//
+//                // 费用生效区间与账单月做交集:
+//                // - 无开始日:视为本月月初
+//                // - 无结束日:视为本月月末
+//                LocalDate periodStart = changeStartDate == null ? billStart : changeStartDate;
+//                LocalDate periodEnd = changeEndDate == null ? billEnd : changeEndDate;
+//
+//                LocalDate overlapStart = periodStart.isAfter(billStart) ? periodStart : billStart;
+//                LocalDate overlapEnd = periodEnd.isBefore(billEnd) ? periodEnd : billEnd;
+//
+//                if (overlapEnd.isBefore(overlapStart)) {
+//                    // 该费用在账单月无覆盖,跳过
+//                    continue;
+//                }
+//
+//                long actualDays = ChronoUnit.DAYS.between(overlapStart, overlapEnd) + 1;
+//                if (daysInBillMonth > 0 && actualDays > 0 && actualDays != daysInBillMonth) {
+//                    BigDecimal dayStandard = standardAmount
+//                            .divide(BigDecimal.valueOf(daysInBillMonth), 8, RoundingMode.HALF_UP);
+//                    billedStandardAmount = dayStandard
+//                            .multiply(BigDecimal.valueOf(actualDays))
+//                            .setScale(2, RoundingMode.HALF_UP);
+//                }
+//            }
 
             Integer type = expenseItemDO.getType();
             if ((type != null  && 1 == type) || expenseItemDO.getItemName().contains("床位费")) {
@@ -314,7 +320,7 @@ public class OrderApiImpl implements OrderApi {
             } else if ((type != null  && 3 == type) || expenseItemDO.getItemName().contains("餐饮费")) {
                 dto.setMealAmount(dto.getMealAmount().add(billedStandardAmount));
                 dto.setMealActualAmount(dto.getMealActualAmount().add(actualAmount));
-            } else if ((type != null  && 7 == type) ||  expenseItemDO.getItemName().contains("服务费")) {
+            } else if ((type != null  && 7 == type) ||  expenseItemDO.getItemName().contains("服务费") || expenseItemDO.getItemCategoryName().contains("服务费")) {
                 dto.setServiceAmount(dto.getServiceAmount().add(billedStandardAmount));
                 dto.setServiceActualAmount(dto.getServiceActualAmount().add(actualAmount));
             }
@@ -408,7 +414,7 @@ public class OrderApiImpl implements OrderApi {
                         .append("-")
                         .append(feeLabel)
                         .append(":")
-                        .append(adjustAmount.toPlainString())
+                        .append(adjustAmount.setScale(2,RoundingMode.HALF_UP).toPlainString())
                         .append(";");
             }
         }
@@ -440,7 +446,7 @@ public class OrderApiImpl implements OrderApi {
             dto.setExtraServiceAmount(dto.getExtraServiceAmount().add(extraAmount));
         }
         dto.setAdjustAmount(dto.getAdjustAmount().add(extraAmount));
-        dto.setRemark((dto.getRemark() == null ? "" : dto.getRemark()) + dailyExpensesDO.getRemarks() + itemName + ":" + dailyExpensesDO.getAmount());
+        dto.setRemark((dto.getRemark() == null ? "" : dto.getRemark()) + (dailyExpensesDO.getRemarks() == null ? "" :  dailyExpensesDO.getRemarks()) + itemName + ":" + dailyExpensesDO.getAmount().setScale(2,RoundingMode.HALF_UP) + ";");
     }
 
     /** 判断是否为水电费相关项目 */
@@ -1289,7 +1295,7 @@ public class OrderApiImpl implements OrderApi {
     @Override
     public List<OrderItemRespVO> getOrderSubListForReport(Integer orgType, Long tenantId, String billingMonth, Integer payStatus) {
         List<OrderItemRespVO> listByMonth = getListByMonth(orgType, tenantId, billingMonth, null, payStatus);
-        if (CollectionUtils.isEmpty(listByMonth)) {
+        if (StringUtil.isEmptyORNull(billingMonth)) {
             return listByMonth;
         }
 
@@ -1297,12 +1303,65 @@ public class OrderApiImpl implements OrderApi {
         LocalDate monthStart = YearMonth.parse(billingMonth, formatter).atDay(1);
         LocalDate monthEnd = monthStart.withDayOfMonth(monthStart.lengthOfMonth());
 
-        // 批量查询离住记录,避免逐条查询导致性能问题
-        Set<Long> elderIds = listByMonth.stream()
-                .map(OrderItemRespVO::getId)
-                .filter(Objects::nonNull)
+        Set<Long> elderIdsInBill = CollectionUtils.isEmpty(listByMonth) ? Collections.emptySet() :
+                listByMonth.stream().map(OrderItemRespVO::getId).filter(Objects::nonNull).collect(Collectors.toSet());
+
+        Map<Long, List<ElderlyTempOutDO>> tempOutMap = fetchTempOutRecordsByTenant(tenantId, monthStart, monthEnd);
+        if (tempOutMap != null && !tempOutMap.isEmpty() && orgType != null) {
+            Set<Long> affectedElderIds = tempOutMap.keySet();
+            List<ElderlyInfoDO> orgFiltered = elderlyInfoMapper.selectList(
+                    new LambdaQueryWrapperX<ElderlyInfoDO>()
+                            .in(ElderlyInfoDO::getId, affectedElderIds)
+                            .eq(ElderlyInfoDO::getTenantId, tenantId)
+                            .eq(ElderlyInfoDO::getOrgType, orgType)
+            );
+            Set<Long> allow = CollectionUtils.isEmpty(orgFiltered) ? Collections.emptySet()
+                    : orgFiltered.stream().map(ElderlyInfoDO::getId).filter(Objects::nonNull).collect(Collectors.toSet());
+            if (CollectionUtils.isEmpty(allow)) {
+                tempOutMap = Collections.emptyMap();
+            } else {
+                Map<Long, List<ElderlyTempOutDO>> filtered = new HashMap<>();
+                for (Map.Entry<Long, List<ElderlyTempOutDO>> e : tempOutMap.entrySet()) {
+                    if (allow.contains(e.getKey())) {
+                        filtered.put(e.getKey(), e.getValue());
+                    }
+                }
+                tempOutMap = filtered;
+            }
+        }
+
+        Set<Long> wholeMonthLeaveElderIds = tempOutMap.entrySet().stream()
+                .filter(e -> isWholeMonthLeaveByTempOut(e.getValue(), monthStart, monthEnd))
+                .map(Map.Entry::getKey)
                 .collect(Collectors.toSet());
-        Map<Long, List<ElderlyAskLeaveDO>> leaveMap = fetchLeaveRecordsByElders(elderIds, monthStart, monthEnd,tenantId);
+
+        if (CollectionUtils.isNotEmpty(wholeMonthLeaveElderIds)) {
+            Set<Long> missing = wholeMonthLeaveElderIds.stream()
+                    .filter(id -> !elderIdsInBill.contains(id))
+                    .collect(Collectors.toSet());
+            if (CollectionUtils.isNotEmpty(missing)) {
+                List<ElderlyInfoDO> elderlyInfos = elderlyInfoMapper.selectBatchIds(missing);
+                if (CollectionUtils.isNotEmpty(elderlyInfos)) {
+                    List<OrderItemRespVO> missingVos = new ArrayList<>();
+                    for (ElderlyInfoDO info : elderlyInfos) {
+                        if (info == null || info.getId() == null) {
+                            continue;
+                        }
+                        missingVos.add(buildWholeMonthLeaveWithoutBillVO(info, billingMonth));
+                    }
+                    if (CollectionUtils.isNotEmpty(missingVos)) {
+                        if (listByMonth == null) {
+                            listByMonth = new ArrayList<>();
+                        }
+                        listByMonth.addAll(missingVos);
+                    }
+                }
+            }
+        }
+
+        if (CollectionUtils.isEmpty(listByMonth)) {
+            return listByMonth;
+        }
 
         for (OrderItemRespVO vo : listByMonth) {
             Long elderId = vo.getId();
@@ -1310,27 +1369,25 @@ public class OrderApiImpl implements OrderApi {
                 continue;
             }
 
-            List<ElderlyAskLeaveDO> leaveList = leaveMap.get(elderId);
-            if (CollectionUtils.isEmpty(leaveList)) {
+            List<ElderlyTempOutDO> outList = tempOutMap.get(elderId);
+            if (CollectionUtils.isEmpty(outList)) {
                 continue;
             }
 
             // 1) 整月离住,分组为“离住”
-            boolean whole = leaveList.stream().anyMatch(r -> isWholeMonthOutbound(r, monthStart, monthEnd))
-                    || isWholeMonthByMultipleOutbounds(leaveList, monthStart, monthEnd);
+            boolean whole = isWholeMonthLeaveByTempOut(outList, monthStart, monthEnd);
             if (whole) {
                 vo.setInStatus("离住");
+                keepMonthlyFeesOnly(vo);
                 continue;
             }
 
             boolean hasEndInMonth = false;
             boolean hasStartInMonthAndNotEnd = false;
 
-            for (ElderlyAskLeaveDO leave : leaveList) {
-                LocalDate outDate = leave.getOutDate() == null ? null :
-                        leave.getOutDate().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
-                LocalDate backDate = leave.getUpdateDate() == null ? null :
-                        leave.getUpdateDate().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
+            for (ElderlyTempOutDO out : outList) {
+                LocalDate outDate = out.getOutTime() == null ? null : out.getOutTime().toLocalDate();
+                LocalDate backDate = out.getComeTime() == null ? null : out.getComeTime().toLocalDate();
 
                 if (backDate != null && !backDate.isBefore(monthStart) && !backDate.isAfter(monthEnd)) {
                     hasEndInMonth = true;
@@ -1362,6 +1419,156 @@ public class OrderApiImpl implements OrderApi {
         return listByMonth;
     }
 
+    private Map<Long, List<ElderlyTempOutDO>> fetchTempOutRecordsByTenant(Long tenantId,
+                                                                          LocalDate monthStart,
+                                                                          LocalDate monthEnd) {
+        if (tenantId == null) {
+            return Collections.emptyMap();
+        }
+        LocalDateTime monthStartDt = monthStart.atStartOfDay();
+        LocalDateTime monthEndDt = monthEnd.atTime(23, 59, 59);
+
+        List<ElderlyTempOutDO> records = elderlyTempOutMapper.selectList(
+                new LambdaQueryWrapperX<ElderlyTempOutDO>()
+                        .eq(ElderlyTempOutDO::getTenantId, tenantId)
+                        .le(ElderlyTempOutDO::getOutTime, monthEndDt)
+                        .and(w -> w.ge(ElderlyTempOutDO::getComeTime, monthStartDt).or().isNull(ElderlyTempOutDO::getComeTime))
+                        .orderByAsc(ElderlyTempOutDO::getElderId, ElderlyTempOutDO::getOutTime)
+        );
+        if (CollectionUtils.isEmpty(records)) {
+            return Collections.emptyMap();
+        }
+        return records.stream().collect(Collectors.groupingBy(ElderlyTempOutDO::getElderId));
+    }
+
+    private boolean isWholeMonthLeaveByTempOut(List<ElderlyTempOutDO> outList, LocalDate monthStart, LocalDate monthEnd) {
+        if (CollectionUtils.isEmpty(outList)) {
+            return false;
+        }
+        for (ElderlyTempOutDO out : outList) {
+            if (out == null || out.getOutTime() == null) {
+                continue;
+            }
+            LocalDate outDate = out.getOutTime().toLocalDate();
+            LocalDate comeDate = out.getComeTime() == null ? null : out.getComeTime().toLocalDate();
+            if (!outDate.isAfter(monthStart) && (comeDate == null || comeDate.isAfter(monthEnd))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private OrderItemRespVO buildWholeMonthLeaveWithoutBillVO(ElderlyInfoDO info, String billingMonth) {
+        OrderItemRespVO vo = new OrderItemRespVO();
+        vo.setId(info.getId());
+        vo.setElderName(info.getElderName() == null ? "" : info.getElderName());
+        vo.setBillingMonth(billingMonth);
+        vo.setElderSex(toSexStr(info.getElderSex()));
+        vo.setElderAge(info.getElderAge() == null ? 0 : info.getElderAge());
+        vo.setBuildName(info.getBuildName() == null ? "" : info.getBuildName());
+        String fullBedName = buildService == null ? null : buildService.getFullBedName(info.getId());
+        vo.setBedName(StringUtil.isEmptyORNull(fullBedName) ? (info.getBedName() == null ? "" : info.getBedName()) : fullBedName);
+        vo.setNurseLevelName(info.getNurseLevelName() == null ? "" : info.getNurseLevelName());
+        vo.setCareType(toCareTypeStr(info.getCareType()));
+        vo.setAddress(info.getAddress() == null ? "" : info.getAddress());
+        vo.setTenantId(info.getTenantId() == null ? null : info.getTenantId().intValue());
+        vo.setOrgType(info.getOrgType());
+
+        vo.setInStatus("离住");
+        vo.setPayStatus("");
+        vo.setRemark("");
+
+        vo.setBedAmount(BigDecimal.ZERO);
+        vo.setMealAmount(BigDecimal.ZERO);
+        vo.setNurseAmount(BigDecimal.ZERO);
+        vo.setServiceAmount(BigDecimal.ZERO);
+        vo.setTotalAmount(BigDecimal.ZERO);
+        vo.setAdjustAmount(BigDecimal.ZERO);
+
+        vo.setBedActualAmount(BigDecimal.ZERO);
+        vo.setMealActualAmount(BigDecimal.ZERO);
+        vo.setNurseActualAmount(BigDecimal.ZERO);
+        vo.setServiceActualAmount(BigDecimal.ZERO);
+        vo.setTotalActualAmount(BigDecimal.ZERO);
+        vo.setExtraServiceAmount(BigDecimal.ZERO);
+        vo.setActualProportion("0%");
+
+        List<ExpenseItemDO> monthlyItems = itemMapper.generateMonthlyBill(info.getId(), billingMonth, 0);
+        if (CollectionUtils.isEmpty(monthlyItems)) {
+            monthlyItems = itemMapper.generateMonthlyBill(info.getId(), billingMonth, 1);
+        }
+        if (CollectionUtils.isNotEmpty(monthlyItems)) {
+            for (ExpenseItemDO item : monthlyItems) {
+                if (item == null) {
+                    continue;
+                }
+                Integer type = item.getType();
+                BigDecimal amt = item.getTotalAmount() == null ? BigDecimal.ZERO : item.getTotalAmount();
+                if (type != null && type == 1) {
+                    vo.setBedAmount(vo.getBedAmount().add(amt));
+                    vo.setBedActualAmount(vo.getBedActualAmount().add(amt));
+                } else if (type != null && type == 2) {
+                    vo.setNurseAmount(vo.getNurseAmount().add(amt));
+                    vo.setNurseActualAmount(vo.getNurseActualAmount().add(amt));
+                } else if (type != null && type == 3) {
+                    vo.setMealAmount(vo.getMealAmount().add(amt));
+                    vo.setMealActualAmount(vo.getMealActualAmount().add(amt));
+                } else if (type != null && type == 7) {
+                    vo.setServiceAmount(vo.getServiceAmount().add(amt));
+                    vo.setServiceActualAmount(vo.getServiceActualAmount().add(amt));
+                }
+            }
+            BigDecimal standardTotal = vo.getBedAmount().add(vo.getMealAmount()).add(vo.getNurseAmount()).add(vo.getServiceAmount());
+            vo.setTotalAmount(standardTotal);
+            vo.setTotalActualAmount(standardTotal);
+            vo.setActualProportion(standardTotal.compareTo(BigDecimal.ZERO) > 0 ? "100%" : "0%");
+        }
+        return vo;
+    }
+
+    private void keepMonthlyFeesOnly(OrderItemRespVO vo) {
+        if (vo == null) {
+            return;
+        }
+        vo.setAdjustAmount(BigDecimal.ZERO);
+        vo.setExtraServiceAmount(BigDecimal.ZERO);
+        BigDecimal total = nz(vo.getBedAmount()).add(nz(vo.getMealAmount())).add(nz(vo.getNurseAmount())).add(nz(vo.getServiceAmount()));
+        vo.setTotalAmount(total);
+        BigDecimal actualTotal = nz(vo.getBedActualAmount()).add(nz(vo.getMealActualAmount())).add(nz(vo.getNurseActualAmount())).add(nz(vo.getServiceActualAmount()));
+        vo.setTotalActualAmount(actualTotal);
+        if (total.compareTo(BigDecimal.ZERO) > 0) {
+            BigDecimal p = actualTotal.divide(total, 4, RoundingMode.HALF_UP)
+                    .multiply(new BigDecimal("100"))
+                    .setScale(2, RoundingMode.HALF_UP);
+            vo.setActualProportion(p.toPlainString() + "%");
+        } else {
+            vo.setActualProportion("0%");
+        }
+    }
+
+    private String toSexStr(Integer elderSex) {
+        if (elderSex == null) {
+            return "";
+        }
+        return elderSex == 1 ? "男" : "女";
+    }
+
+    private String toCareTypeStr(Integer careType) {
+        if (careType == null) {
+            return "";
+        }
+        switch (careType) {
+            case 1:
+                return "供养";
+            case 2:
+                return "寄养";
+            case 3:
+                return "托养";
+            default:
+                return "";
+        }
+    }
+
     /**
      * 批量查询指定长者在某月的“离住”外出记录(有交集的记录),并按 elderId 分组
      */

+ 3 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/ElderlyInfoPageReqVO.java

@@ -42,6 +42,9 @@ public class ElderlyInfoPageReqVO extends PageParam {
     @Schema(description = "入住类型", example = "1")
     private Integer inStatusType;
 
+    @Schema(description = "供养类型(照护类型:1供养 2寄养 3托养)", example = "1")
+    private Integer careType;
+
     @Schema(description = "办理状态", example = "状态,1:待入住办理 2:已入住")
     private Integer status;
 

+ 4 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/excel/ElderlyBaseInfoExportExcelVO.java

@@ -101,6 +101,9 @@ public class ElderlyBaseInfoExportExcelVO {
     @DictFormat(DictTypeConstants.FORM_SUPPLY_ARR)
     private Integer formSupply;
 
+    @ExcelProperty("供养类型")
+    private String careType;
+
     @ExcelProperty(value = "医疗保险",converter = DictConvert.class)
     @DictFormat(DictTypeConstants.MEDICAL_INSURANCE_ARR)
     private String medicalInsurance;
@@ -217,4 +220,4 @@ public class ElderlyBaseInfoExportExcelVO {
     @ExcelProperty("第二托养人家庭住址")
     private String secondRelativeAddress;
 
-}
+}

+ 19 - 17
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyInfoServiceImpl.java

@@ -355,22 +355,8 @@ public class ElderlyInfoServiceImpl implements ElderlyInfoService {
             elderlyInfo.setChineseCalendar(chineseCalendar);
         }
         elderlyInfoMapper.updateById(elderlyInfo);
-
-
-        if (elderlyInfo.getRelativesList() != null) {
-            for (RelativesDO relatives : elderlyInfo.getRelativesList()) {
-                if (relatives.getId() != null){
-                    relativesMapper.updateById(relatives);
-                }else {
-                    relatives.setPeopleId(elderlyInfo.getId());
-                    relatives.setPeopleName(elderlyInfo.getElderName());
-                    relatives.setStatus("0");
-                    relatives.setCreateDate(LocalDateTime.now());
-                    relativesMapper.insert(relatives);
-                }
-            }
-        }
-
+        relativesMapper.delete(new LambdaQueryWrapperX<RelativesDO>().eq(RelativesDO::getPeopleId,elderlyInfo.getId()));
+        relativesMapper.insertBatch(elderlyInfo.getRelativesList());
     }
 
     @Override
@@ -825,6 +811,7 @@ public class ElderlyInfoServiceImpl implements ElderlyInfoService {
             ElderlyBaseInfoExportExcelVO baseInfoExportExcelVO = new ElderlyBaseInfoExportExcelVO();
             BeanUtils.copyProperties(e, baseInfoExportExcelVO);
             baseInfoExportExcelVO.setNum(atomicInteger.getAndIncrement());
+            baseInfoExportExcelVO.setCareType(formatCareType(e.getCareType()));
             baseInfoExportExcelVO.setBirthdayCareDate(e.getBirthday() == null ? null : e.getBirthday().toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
             baseInfoExportExcelVO.setBedName(buildService.getFullBedName(e.getId()));
             if (e.getNurseLevelId() != null) {
@@ -864,6 +851,22 @@ public class ElderlyInfoServiceImpl implements ElderlyInfoService {
         return dataMap;
     }
 
+    private static String formatCareType(Integer careType) {
+        if (careType == null) {
+            return null;
+        }
+        switch (careType) {
+            case 1:
+                return "供养";
+            case 2:
+                return "寄养";
+            case 3:
+                return "托养";
+            default:
+                return String.valueOf(careType);
+        }
+    }
+
     @Override
     @TenantIgnore
     public CheckInRecordDetailRespVO getCheckInDetail(Long id) {
@@ -3055,4 +3058,3 @@ public class ElderlyInfoServiceImpl implements ElderlyInfoService {
 
 
 
-

+ 50 - 30
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ExpenseOrderServiceImpl.java

@@ -1296,7 +1296,7 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
             for (int i = 0; i < orderItems.size(); i++) {
                 ExpenseOrderItemDO expenseOrderItemDO = orderItems.get(i);
                 orderActualAmount = orderActualAmount.add(expenseOrderItemDO.getTotalAmount());
-                // 若明细未带 type,则根据“费用标识类型”字典的关键词(label/remark)从名称中推断 type
+                // 若明细未带 type,则根据“费用标识类型”字典的关键词(label/remark)推断 type:优先匹配项目名称,其次匹配项目类型/分类
                 setExpenseOrderType(expenseOrderItemDO, dictDataDOList);
 
                 if (identificationTypeMap.containsKey(expenseOrderItemDO.getType())) {
@@ -3606,8 +3606,12 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
      *     <li>{@link DictDataDO#getRemark()}:可选,同义词/额外关键词,支持用 ,,;;| 分隔</li>
      * </ul>
      *
-     * <p>匹配字段:优先在 {@code itemName} 命中,其次 {@code itemCategoryName}。
-     * 另外对包含“标准/类型”的关键词,会自动尝试去掉“标准/类型”后的匹配,减少配置成本。
+     * <p>匹配字段:先全量匹配 {@code itemName}(项目名称);仅当 {@code itemName} 无任何命中时,才匹配
+     * {@code itemCategoryName}(项目类型/分类)。
+     *
+     * <p>同一字段多命中时:优先选择命中关键词更长的字典项,避免短关键词“抢占”导致误分类。
+     *
+     * <p>关键词扩展:对包含“标准/类型”的关键词,会自动补充去掉“标准/类型”后的版本(例如“服务标准”->“服务”),减少配置成本。
      */
     private void setExpenseOrderType(ExpenseOrderItemDO expenseOrderItemDO, List<DictDataDO> identificationTypeDictList) {
         // 明细已带 type 时不处理,避免覆盖正确值
@@ -3622,34 +3626,56 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
         String itemName = StringUtils.defaultString(expenseOrderItemDO.getItemName());
         String itemCategoryName = StringUtils.defaultString(expenseOrderItemDO.getItemCategoryName());
 
+        Integer matchedType = inferIdentificationTypeByText(itemName, identificationTypeDictList);
+        if (matchedType != null) {
+            expenseOrderItemDO.setType(matchedType);
+            return;
+        }
+        matchedType = inferIdentificationTypeByText(itemCategoryName, identificationTypeDictList);
+        if (matchedType != null) {
+            expenseOrderItemDO.setType(matchedType);
+        }
+    }
+
+    private Integer inferIdentificationTypeByText(String text, List<DictDataDO> identificationTypeDictList) {
+        if (StringUtils.isBlank(text) || CollectionUtil.isEmpty(identificationTypeDictList)) {
+            return null;
+        }
+
+        Integer bestType = null;
+        int bestKeywordLength = -1;
+        int bestSort = Integer.MAX_VALUE;
+
         for (DictDataDO dictDataDO : identificationTypeDictList) {
-            if (dictDataDO == null) {
+            if (dictDataDO == null || StringUtils.isBlank(dictDataDO.getValue())) {
                 continue;
             }
-            if (StringUtils.isBlank(dictDataDO.getValue())) {
+
+            Integer type;
+            try {
+                type = Integer.valueOf(dictDataDO.getValue());
+            } catch (NumberFormatException ignored) {
                 continue;
             }
-            // 收集候选关键词:label + remark(分隔后)
-            List<String> keywords = new ArrayList<>();
+
+            Set<String> expandedKeywords = new HashSet<>();
             if (StringUtils.isNotBlank(dictDataDO.getLabel())) {
-                keywords.add(dictDataDO.getLabel());
+                expandedKeywords.add(dictDataDO.getLabel().trim());
             }
             if (StringUtils.isNotBlank(dictDataDO.getRemark())) {
                 String[] remarkKeywords = dictDataDO.getRemark().split("[,,;;|]");
                 for (String keyword : remarkKeywords) {
                     if (StringUtils.isNotBlank(keyword)) {
-                        keywords.add(keyword.trim());
+                        expandedKeywords.add(keyword.trim());
                     }
                 }
             }
 
-            // 扩展关键词:自动补充去掉“标准/类型”后的版本(例如“服务标准”->“服务”)
-            List<String> expandedKeywords = new ArrayList<>(keywords.size() * 2);
-            for (String keyword : keywords) {
+            List<String> keywordsSnapshot = new ArrayList<>(expandedKeywords);
+            for (String keyword : keywordsSnapshot) {
                 if (StringUtils.isBlank(keyword)) {
                     continue;
                 }
-                expandedKeywords.add(keyword);
                 if (keyword.contains("标准")) {
                     expandedKeywords.add(keyword.replace("标准", ""));
                 }
@@ -3658,32 +3684,26 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
                 }
             }
             expandedKeywords.removeIf(StringUtils::isBlank);
-            // 先匹配更长的关键词,避免短关键词抢占(例如“护理”比“护理用品费”更容易误命中)
-            expandedKeywords.sort((a, b) -> Integer.compare(b.length(), a.length()));
 
-            boolean matched = false;
+            int matchedKeywordLength = -1;
             for (String keyword : expandedKeywords) {
-                if (StringUtils.isNotBlank(itemName) && itemName.contains(keyword)) {
-                    matched = true;
-                    break;
-                }
-                if (StringUtils.isNotBlank(itemCategoryName) && itemCategoryName.contains(keyword)) {
-                    matched = true;
-                    break;
+                if (StringUtils.isNotBlank(keyword) && text.contains(keyword)) {
+                    matchedKeywordLength = Math.max(matchedKeywordLength, keyword.length());
                 }
             }
-            if (!matched) {
+            if (matchedKeywordLength < 0) {
                 continue;
             }
 
-            try {
-                // 命中后写入 type,供后续汇总统计使用
-                expenseOrderItemDO.setType(Integer.valueOf(dictDataDO.getValue()));
-                return;
-            } catch (NumberFormatException ignored) {
-                // value 不是数字时忽略该字典项,继续尝试下一项
+            int dictSort = dictDataDO.getSort() == null ? Integer.MAX_VALUE : dictDataDO.getSort();
+            if (matchedKeywordLength > bestKeywordLength
+                    || (matchedKeywordLength == bestKeywordLength && dictSort < bestSort)) {
+                bestKeywordLength = matchedKeywordLength;
+                bestSort = dictSort;
+                bestType = type;
             }
         }
+        return bestType;
     }
 
     private List<ExpenseItemRespVO> handlerCollectMonthlyExpense(String billingMonth, Long elderId){

+ 3 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/RefundSettlementOrderServiceImpl.java

@@ -1774,6 +1774,9 @@ public class RefundSettlementOrderServiceImpl implements RefundSettlementOrderSe
                     case 3: // 外出费用
                         processChangeOrOutgoingExpense(dailyExpenses, costDeadlineDate, itemVO);
                         break;
+                    case 4:
+                        itemVO.setSkip(true);
+                        return;
                     default:
                         itemVO.setDays(1);
                         break;

+ 1 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java

@@ -187,6 +187,7 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService {
                 .setUserId(userId).setUserType(userType)
                 .setClientId(clientDO.getClientId()).setScopes(scopes)
                 .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getRefreshTokenValiditySeconds()));
+        refreshToken.setTenantId(TenantContextHolder.getTenantId());
         oauth2RefreshTokenMapper.insert(refreshToken);
         return refreshToken;
     }

+ 11 - 3
yudao-module-system/yudao-module-system-biz/src/main/resources/mapper/ElderlyConsumerVouchersMapper.xml

@@ -7,9 +7,13 @@
         select ecv.*,
                ei.elder_name as elderName,
                ei.id_card as idCard,
-               ei.bed_name as bedName
+               CONCAT(b.build_name,'-',f.floor_name,'-',r.room_name,'-',bed.bed_name) as bedName
         from elderly_consumer_vouchers ecv
                  left join elderly_info ei on ei.id = ecv.elder_id
+                 left join org_build b on ei.build_id = b.id
+                 left join org_build_floor f on ei.floor_id = f.id
+                 left join org_build_room r on ei.room_id = r.id
+                 left join org_build_bed bed on ei.bed_id = bed.id
         <where>
             1 = 1
             <if test="reqVO.elderName != null and reqVO.elderName != ''">
@@ -42,9 +46,13 @@
         select ecv.*,
                ei.elder_name as elderName,
                ei.id_card as idCard,
-               ei.bed_name as bedName
+               CONCAT(b.build_name,'-',f.floor_name,'-',r.room_name,'-',bed.bed_name) as bedName
         from elderly_consumer_vouchers ecv
-                 left join elderly_info ei on ei.id = ecv.elder_id
+                left join elderly_info ei on ei.id = ecv.elder_id
+                left join org_build b on ei.build_id = b.id
+                left join org_build_floor f on ei.floor_id = f.id
+                left join org_build_room r on ei.room_id = r.id
+                left join org_build_bed bed on ei.bed_id = bed.id
         where ecv.id = #{id}
         limit 1
     </select>

+ 3 - 0
yudao-module-system/yudao-module-system-biz/src/main/resources/mapper/ElderlyInfoMapper.xml

@@ -99,6 +99,9 @@
         <if test="pageVO.nurseLevelId != null">
             and ei.nurse_level_id = #{pageVO.nurseLevelId}
         </if>
+        <if test="pageVO.careType != null">
+            and ei.care_type = #{pageVO.careType}
+        </if>
         <if test="pageVO.checkInTime != null">
             and ei.create_time between #{pageVO.checkInTime[0]} and #{pageVO.checkInTime[1]}
         </if>