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

增加
1、新增查询指定日期、多个时间段的预约名额情况接口
2、外出模块新增出院扫码接口
3、外出模块新增回院扫码接口
BUGFIX
1、解决床位对调没有针对账单费用进行退补处理的问题

liangwenxuan 1 місяць тому
батько
коміт
ebb20ac865
25 змінених файлів з 664 додано та 21 видалено
  1. 1 0
      sql/mysql/appointment_config.sql
  2. 2 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
  3. 2 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/change/ChangeConstants.java
  4. 20 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/ElderlyAskLeaveController.java
  5. 3 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/JiashuVisitAppointmentController.java
  6. 2 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/RefundSettlementOrderController.java
  7. 23 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/RefundSettlementOrderPageRespVO.java
  8. 4 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/excel/ElderlyBaseInfoExportExcelVO.java
  9. 11 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/appointment/AppAppointmentQuotaController.java
  10. 35 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/appointment/vo/AppointmentQuotaQueryReqVO.java
  11. 29 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/appointment/vo/AppointmentQuotaQueryRespVO.java
  12. 5 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/biz/AppointmentGroupDO.java
  13. 15 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/biz/ElderlyAskLeaveDO.java
  14. 3 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/miniapp/JiashuVisitAppointmentMapper.java
  15. 13 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/appointment/AppAppointmentQuotaService.java
  16. 111 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/appointment/AppAppointmentQuotaServiceImpl.java
  17. 9 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/AppointmentConfigServiceImpl.java
  18. 14 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyAskLeaveService.java
  19. 33 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyAskLeaveServiceImpl.java
  20. 304 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/NurseChangeRecordServiceImpl.java
  21. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/RefundSettlementOrderService.java
  22. 10 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/RefundSettlementOrderServiceImpl.java
  23. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/miniapp/JiashuVisitAppointmentService.java
  24. 10 10
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/miniapp/JiashuVisitAppointmentServiceImpl.java
  25. 3 0
      yudao-server/src/main/resources/application.yaml

+ 1 - 0
sql/mysql/appointment_config.sql

@@ -33,5 +33,6 @@ CREATE TABLE `appointment_group`  (
   `creator` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
   `updater` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人',
   `tenant_id` bigint(20) NULL DEFAULT NULL COMMENT '机构id',
+  `enable` tinyint(1) NOT NULL DEFAULT 1 COMMENT '使用状态:0-禁用,1-启用',
   PRIMARY KEY (`id`) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '预约组表' ROW_FORMAT = Dynamic;

+ 2 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java

@@ -277,6 +277,8 @@ public interface ErrorCodeConstants {
     ErrorCode ELDERLY_RENEW_ORDER_NOT_EXISTS = new ErrorCode(1_008_004_000, "长者不存在续签月账单,请先生成!");
     // ========== 外出相关 1-008-005-000 ==========
     ErrorCode ELDERLY_LEAVE_UPDATE_DATE_OVER_EXPIRE = new ErrorCode(1_008_005_000, "返院时间大于合同到期时间,请检查!");
+    ErrorCode ELDERLY_LEAVE_SCAN_OUT_NOT_FOUND = new ErrorCode(1_008_005_001, "未找到外出时间为空的外出记录,无法进行出院扫码!");
+    ErrorCode ELDERLY_LEAVE_SCAN_RETURN_NOT_FOUND = new ErrorCode(1_008_005_002, "未找到实际返回时间为空的外出记录,无法进行回院扫码!");
     // ========== 菜品相关 1-008-006-000 ==========
     ErrorCode DISHES_EXISTS = new ErrorCode(1_008_006_000, "该菜品已存在,请检查!");
     // ========== 风险知情书 1-008-007-000 ==========

+ 2 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/change/ChangeConstants.java

@@ -18,6 +18,8 @@ public interface ChangeConstants {
 
     String CATERING_CHANGE_DAILY_PAY_REMARK =  "餐饮价格变更:合同价格:%s 元/月,原价格:%s 元/月,新价格:%s元/月,原价格使用天数:%s 天,新价格使用天数:%s 天";
 
+    String BED_SWAP_DAILY_PAY_REMARK =  "床位对调费用变更:合同价格:%s 元/月,原价格:%s 元/月,新价格:%s元/月,原价格使用天数:%s 天,新价格使用天数:%s 天";
+
     String PRICE_CHANGE_DAILY_PAY_REMARK =  "价格变更:合同价格:%s 元/月,原价格:%s 元/月,新价格:%s元/月,原价格使用天数:%s 天,新价格使用天数:%s 天";
 
 //    String PRICE_CHANGE_DAILY_REFUND_REMARK =  "%s日%s类型发生价格变更,变更前月费用为%s元;变更后月费用为%s元,%s至%s日产生金额为%s元,变更后%s至%s日应付%s元";

+ 20 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/ElderlyAskLeaveController.java

@@ -113,6 +113,26 @@ public class ElderlyAskLeaveController {
     }
 
 
+    @PostMapping("/scanOut")
+    @Operation(summary = "出院扫码 - 更新外出时间为当前时间")
+    @TenantIgnore
+    @PermitAll
+    public CommonResult<Boolean> scanOut(@RequestParam("elderId") Long elderId) {
+        elderlyAskLeaveService.scanOut(elderId);
+        return success(true);
+    }
+
+
+    @PostMapping("/scanReturn")
+    @Operation(summary = "回院扫码 - 更新实际返回时间为当前时间")
+    @TenantIgnore
+    @PermitAll
+    public CommonResult<Boolean> scanReturn(@RequestParam("elderId") Long elderId) {
+        elderlyAskLeaveService.scanReturn(elderId);
+        return success(true);
+    }
+
+
 //
 //    @PutMapping("/update")
 //    @Operation(summary = "更新老人请假管理")

+ 3 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/JiashuVisitAppointmentController.java

@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 import javax.annotation.Resource;
@@ -28,8 +29,8 @@ public class JiashuVisitAppointmentController {
 
     @GetMapping("/count")
     @Operation(summary = "获取当日、当周、当月的预约参观数量")
-    public CommonResult<VisitAppointmentCountRespVO> count() {
-        return success(jiashuVisitAppointmentService.getVisitCount());
+    public CommonResult<VisitAppointmentCountRespVO> count(@RequestParam(name = "tenantId") Long tenantId) {
+        return success(jiashuVisitAppointmentService.getVisitCount(tenantId));
     }
 
 }

+ 2 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/RefundSettlementOrderController.java

@@ -78,9 +78,9 @@ public class RefundSettlementOrderController {
     @GetMapping("/getRefundSettlementOrderPage")
     @Operation(summary = "获得长者退住结算单分页")
     @TenantIgnore
-    public CommonResult<PageResult<RefundSettlementOrderDO>> getRefundSettlementOrderPage(@Valid RefundSettlementOrderPageReqVO pageReqVO) {
+    public CommonResult<PageResult<RefundSettlementOrderPageRespVO>> getRefundSettlementOrderPage(@Valid RefundSettlementOrderPageReqVO pageReqVO) {
         pageReqVO.setTenantIds(Objects.isNull(pageReqVO.getTenantIds()) ? new Long[]{TenantContextHolder.getTenantId()} : pageReqVO.getTenantIds());
-        PageResult<RefundSettlementOrderDO> pageResult = refundSettlementOrderService.getRefundSettlementOrderPage(pageReqVO);
+        PageResult<RefundSettlementOrderPageRespVO> pageResult = refundSettlementOrderService.getRefundSettlementOrderPage(pageReqVO);
         return success(pageResult);
     }
 

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

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.system.controller.admin.biz.vo;
+
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.RefundSettlementOrderDO;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+
+import java.time.LocalDate;
+
+@Schema(description = "管理后台 - 长者退住结算单分页 Response VO")
+@Data
+@ToString(callSuper = true)
+@EqualsAndHashCode(callSuper = true)
+@NoArgsConstructor
+@AllArgsConstructor
+public class RefundSettlementOrderPageRespVO extends RefundSettlementOrderDO {
+
+    @Schema(description = "退住日期")
+    private LocalDate retreatDate;
+
+    @Schema(description = "费用截止日期")
+    private LocalDate costDeadlineDate;
+
+}

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

@@ -155,6 +155,10 @@ public class ElderlyBaseInfoExportExcelVO {
     @DateTimeFormat("yyyy-MM-dd")
     private Date checkInTime;
 
+    @ExcelProperty("退住日期")
+    @DateTimeFormat("yyyy-MM-dd")
+    private Date checkOutTime;
+
     @ExcelProperty("护理等级")
     private String nurseLevelName;
 

+ 11 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/appointment/AppAppointmentQuotaController.java

@@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import javax.annotation.security.PermitAll;
 import cn.iocoder.yudao.module.system.controller.app.appointment.vo.AppointmentQuotaOccupyReqVO;
 import cn.iocoder.yudao.module.system.controller.app.appointment.vo.AppointmentQuotaOccupyRespVO;
+import cn.iocoder.yudao.module.system.controller.app.appointment.vo.AppointmentQuotaQueryReqVO;
+import cn.iocoder.yudao.module.system.controller.app.appointment.vo.AppointmentQuotaQueryRespVO;
 import cn.iocoder.yudao.module.system.service.appointment.AppAppointmentQuotaService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -16,6 +18,8 @@ import org.springframework.web.bind.annotation.RestController;
 import javax.annotation.Resource;
 import javax.validation.Valid;
 
+import java.util.List;
+
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 
 /**
@@ -50,4 +54,11 @@ public class AppAppointmentQuotaController {
         return success(true);
     }
 
+    @PostMapping("/query")
+    @Operation(summary = "查询指定日期、多个时间段的预约名额情况(总数/已预约/剩余)")
+    @PermitAll
+    public CommonResult<List<AppointmentQuotaQueryRespVO>> queryQuota(@Valid @RequestBody AppointmentQuotaQueryReqVO reqVO) {
+        return success(appointmentQuotaService.queryQuota(reqVO));
+    }
+
 }

+ 35 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/appointment/vo/AppointmentQuotaQueryReqVO.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.system.controller.app.appointment.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.time.LocalDate;
+import java.util.List;
+
+/**
+ * 用户 App - 预约名额查询 Request VO
+ *
+ * 前端传入预约日期与若干时间段(HH:mm-HH:mm),查询每个时间段的名额情况。
+ *
+ * @author system
+ */
+@Schema(description = "用户 App - 预约名额查询 Request VO")
+@Data
+public class AppointmentQuotaQueryReqVO {
+
+    @Schema(description = "养老机构 id(即预约配置中的 tenantId)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "养老机构 id 不能为空")
+    private Long orgId;
+
+    @Schema(description = "预约日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-07-16")
+    @NotNull(message = "预约日期不能为空")
+    private LocalDate visitDate;
+
+    @Schema(description = "预约时间段数组,每项格式为 HH:mm-HH:mm,如 [\"09:00-10:00\",\"10:00-11:00\"]",
+            requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "预约时间段不能为空")
+    private List<String> timeSlots;
+
+}

+ 29 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/app/appointment/vo/AppointmentQuotaQueryRespVO.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.system.controller.app.appointment.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 用户 App - 预约名额查询 Response VO
+ *
+ * 与请求中的时间段一一对应,返回该时间段的预约总数、已预约数、剩余预约数。
+ *
+ * @author system
+ */
+@Schema(description = "用户 App - 预约名额查询 Response VO")
+@Data
+public class AppointmentQuotaQueryRespVO {
+
+    @Schema(description = "时间段,原样回传,如 09:00-10:00", requiredMode = Schema.RequiredMode.REQUIRED)
+    private String timeSlot;
+
+    @Schema(description = "预约总数(对应预约组的 limitCount);为 null 表示该组不限制人数", example = "10")
+    private Integer total;
+
+    @Schema(description = "已预约数", example = "3")
+    private Integer booked;
+
+    @Schema(description = "剩余预约数;total 为 null 时也为 null(不限制);未匹配到可预约组时为 0", example = "7")
+    private Integer remaining;
+
+}

+ 5 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/biz/AppointmentGroupDO.java

@@ -60,4 +60,9 @@ public class AppointmentGroupDO extends BaseNoDeleteDO {
      */
     private Long tenantId;
 
+    /**
+     * 使用状态:0-禁用,1-启用
+     */
+    private Integer enable;
+
 }

+ 15 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/biz/ElderlyAskLeaveDO.java

@@ -32,6 +32,15 @@ public class ElderlyAskLeaveDO implements Serializable {
     public static final Integer LEAVE_STATUS_REJECTED = 2;
     public static final Integer LEAVE_STATUS_CANCELLED = 3;
 
+    /**
+     * 申请类型:院区申请
+     */
+    public static final Integer REQUEST_TYPE_INSTITUTION = 1;
+    /**
+     * 申请类型:家属申请
+     */
+    public static final Integer REQUEST_TYPE_FAMILY = 2;
+
     /**
      * 主键ID
      */
@@ -149,4 +158,10 @@ public class ElderlyAskLeaveDO implements Serializable {
     @Schema(description = "归属月")
     private String belongMonth;
 
+    /**
+     * 申请类型:1、院区申请,2、家属申请
+     */
+    @Schema(description = "申请类型:1、院区申请,2、家属申请", example = "1")
+    private Integer requestType;
+
 }

+ 3 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/miniapp/JiashuVisitAppointmentMapper.java

@@ -17,10 +17,11 @@ public interface JiashuVisitAppointmentMapper extends BaseMapperX<JiashuVisitApp
      * @param endDate   结束日期(含)
      * @return 预约数量
      */
-    default Long countByVisitDateRange(LocalDate startDate, LocalDate endDate) {
+    default Long countByVisitDateRange(LocalDate startDate, LocalDate endDate,Long tenantId) {
         return selectCount(new LambdaQueryWrapperX<JiashuVisitAppointmentDO>()
                 .between(JiashuVisitAppointmentDO::getVisitDate, startDate, endDate)
-                .in(JiashuVisitAppointmentDO::getStatus, 1,3));
+                .in(JiashuVisitAppointmentDO::getStatus, 1,3)
+                .eq(JiashuVisitAppointmentDO::getOrderId,tenantId));
     }
 
     /**

+ 13 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/appointment/AppAppointmentQuotaService.java

@@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.system.service.appointment;
 
 import cn.iocoder.yudao.module.system.controller.app.appointment.vo.AppointmentQuotaOccupyReqVO;
 import cn.iocoder.yudao.module.system.controller.app.appointment.vo.AppointmentQuotaOccupyRespVO;
+import cn.iocoder.yudao.module.system.controller.app.appointment.vo.AppointmentQuotaQueryReqVO;
+import cn.iocoder.yudao.module.system.controller.app.appointment.vo.AppointmentQuotaQueryRespVO;
 
 /**
  * 预约名额预占 Service
@@ -29,4 +31,15 @@ public interface AppAppointmentQuotaService {
      */
     void release(AppointmentQuotaOccupyReqVO reqVO);
 
+    /**
+     * 查询指定日期、多个时间段的预约名额情况(总数/已预约/剩余)
+     *
+     * <p>对输入的每个时间段,匹配到对应的预约组后返回其 limitCount(总数)、
+     * Redis 组维度已预占/已预约数(已预约) 与剩余数;未匹配到可预约组或总开关关闭时返回 0。</p>
+     *
+     * @param reqVO 机构、日期、时间段数组
+     * @return 与输入时间段一一对应的名额情况数组
+     */
+    java.util.List<AppointmentQuotaQueryRespVO> queryQuota(AppointmentQuotaQueryReqVO reqVO);
+
 }

+ 111 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/appointment/AppAppointmentQuotaServiceImpl.java

@@ -5,6 +5,8 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.module.system.controller.admin.biz.vo.AppointmentConfigRespVO;
 import cn.iocoder.yudao.module.system.controller.app.appointment.vo.AppointmentQuotaOccupyReqVO;
 import cn.iocoder.yudao.module.system.controller.app.appointment.vo.AppointmentQuotaOccupyRespVO;
+import cn.iocoder.yudao.module.system.controller.app.appointment.vo.AppointmentQuotaQueryReqVO;
+import cn.iocoder.yudao.module.system.controller.app.appointment.vo.AppointmentQuotaQueryRespVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.biz.AppointmentConfigDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.biz.AppointmentGroupDO;
 import cn.iocoder.yudao.module.system.dal.mysql.miniapp.JiashuVisitAppointmentMapper;
@@ -19,6 +21,7 @@ import java.time.DayOfWeek;
 import java.time.LocalDate;
 import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 
@@ -144,6 +147,53 @@ public class AppAppointmentQuotaServiceImpl implements AppAppointmentQuotaServic
         // 注意:组维度不递减——visit_appointment 缺 groupId 无法从 DB 按组统计,Redis 即正式计数,到期自动释放
     }
 
+    @Override
+    public List<AppointmentQuotaQueryRespVO> queryQuota(AppointmentQuotaQueryReqVO reqVO) {
+        Long tenantId = reqVO.getOrgId();
+        LocalDate visitDate = reqVO.getVisitDate();
+
+        AppointmentConfigRespVO configResp = appointmentConfigService.getByTenant(tenantId);
+        AppointmentConfigDO config = configResp.getConfig();
+        List<AppointmentGroupDO> groups = configResp.getGroup();
+
+        // 总开关关闭或配置不存在:所有时间段均不可预约
+        boolean bookable = config != null && Objects.equals(config.getEnabled(), 1);
+
+        List<AppointmentQuotaQueryRespVO> result = new ArrayList<>();
+        if (reqVO.getTimeSlots() == null) {
+            return result;
+        }
+        for (String slot : reqVO.getTimeSlots()) {
+            AppointmentQuotaQueryRespVO vo = new AppointmentQuotaQueryRespVO();
+            vo.setTimeSlot(slot);
+            if (!bookable) {
+                vo.setTotal(0);
+                vo.setBooked(0);
+                vo.setRemaining(0);
+                result.add(vo);
+                continue;
+            }
+            LocalTime begin = parseSlotBegin(slot);
+            LocalTime end = parseSlotEnd(slot);
+            AppointmentGroupDO matched = matchGroupBySegment(groups, visitDate, begin, end);
+            if (matched == null) {
+                // 未匹配到可预约组:该时间段不可预约
+                vo.setTotal(0);
+                vo.setBooked(0);
+                vo.setRemaining(0);
+            } else {
+                Integer limit = matched.getLimitCount(); // null 表示不限制人数
+                long booked = appointmentQuotaRedisDAO.getCount(
+                        appointmentQuotaRedisDAO.formatGroupDailyKey(matched.getId(), visitDate));
+                vo.setTotal(limit);
+                vo.setBooked((int) booked);
+                vo.setRemaining(limit == null ? null : Math.max(0, limit - (int) booked));
+            }
+            result.add(vo);
+        }
+        return result;
+    }
+
     // ========== 各维度校验 ==========
 
     private void checkTotalLimit(AppointmentConfigDO config, Long tenantId, LocalDate date) {
@@ -235,6 +285,9 @@ public class AppAppointmentQuotaServiceImpl implements AppAppointmentQuotaServic
             return null;
         }
         for (AppointmentGroupDO g : groups) {
+            if (!isEnabled(g)) {
+                continue; // 禁用的预约组不参与匹配
+            }
             if (matchDate(g, date) && matchTime(g, begin, end)) {
                 return g;
             }
@@ -247,6 +300,9 @@ public class AppAppointmentQuotaServiceImpl implements AppAppointmentQuotaServic
             return false;
         }
         for (AppointmentGroupDO g : groups) {
+            if (!isEnabled(g)) {
+                continue; // 禁用的预约组不参与匹配
+            }
             if (matchDate(g, date)) {
                 return true;
             }
@@ -254,6 +310,61 @@ public class AppAppointmentQuotaServiceImpl implements AppAppointmentQuotaServic
         return false;
     }
 
+    private boolean isEnabled(AppointmentGroupDO g) {
+        // null 视为启用,兼容历史数据/未传该字段的情况
+        return g.getEnable() == null || Objects.equals(g.getEnable(), 1);
+    }
+
+    /**
+     * 按时间段匹配预约组:日期匹配 且 该时间段完全落在组的 [beginTime,endTime] 内(组未配置时间段则整体可约)
+     */
+    private AppointmentGroupDO matchGroupBySegment(List<AppointmentGroupDO> groups, LocalDate date, LocalTime begin, LocalTime end) {
+        if (groups == null) {
+            return null;
+        }
+        for (AppointmentGroupDO g : groups) {
+            if (!isEnabled(g)) {
+                continue;
+            }
+            if (!matchDate(g, date)) {
+                continue;
+            }
+            if (!matchSegmentTime(g, begin, end)) {
+                continue;
+            }
+            return g;
+        }
+        return null;
+    }
+
+    private boolean matchSegmentTime(AppointmentGroupDO g, LocalTime begin, LocalTime end) {
+        if (g.getBeginTime() == null || g.getEndTime() == null) {
+            return true; // 组未配置时间段,不做限制
+        }
+        LocalTime gBegin = LocalTime.parse(g.getBeginTime(), TIME_FMT);
+        LocalTime gEnd = LocalTime.parse(g.getEndTime(), TIME_FMT);
+        // 请求时间段须完全落在组时间段内(子集)
+        return !begin.isBefore(gBegin) && !end.isAfter(gEnd);
+    }
+
+    private LocalTime parseSlotBegin(String slot) {
+        try {
+            String[] parts = slot.split("-");
+            return LocalTime.parse(parts[0].trim(), TIME_FMT);
+        } catch (Exception e) {
+            throw exception(ErrorCodeConstants.APPOINTMENT_TIME_NOT_ALLOWED);
+        }
+    }
+
+    private LocalTime parseSlotEnd(String slot) {
+        try {
+            String[] parts = slot.split("-");
+            return LocalTime.parse(parts[1].trim(), TIME_FMT);
+        } catch (Exception e) {
+            throw exception(ErrorCodeConstants.APPOINTMENT_TIME_NOT_ALLOWED);
+        }
+    }
+
     private boolean matchTime(AppointmentGroupDO g, LocalTime begin, LocalTime end) {
         if (g.getBeginTime() == null || g.getEndTime() == null) {
             return true; // 组未配置时间段,不做时间段限制

+ 9 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/AppointmentConfigServiceImpl.java

@@ -39,7 +39,12 @@ public class AppointmentConfigServiceImpl implements AppointmentConfigService {
 
         List<AppointmentGroupDO> groupList = reqVO.getGroup();
         if (!CollectionUtils.isEmpty(groupList)) {
-            groupList.forEach(group -> group.setTenantId(tenantId));
+            groupList.forEach(group -> {
+                group.setTenantId(tenantId);
+                if (group.getEnable() == null) {
+                    group.setEnable(1); // 未传使用状态时默认启用
+                }
+            });
             appointmentGroupMapper.insertBatch(groupList);
         }
     }
@@ -66,6 +71,9 @@ public class AppointmentConfigServiceImpl implements AppointmentConfigService {
             groupList.forEach(group -> {
                 group.setId(null);
                 group.setTenantId(tenantId);
+                if (group.getEnable() == null) {
+                    group.setEnable(1); // 未传使用状态时默认启用
+                }
             });
             appointmentGroupMapper.insertBatch(groupList);
         }

+ 14 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyAskLeaveService.java

@@ -31,4 +31,18 @@ public interface ElderlyAskLeaveService {
      * 获取导出数据列表,复用分页查询逻辑
      */
     List<ElderlyAskLeaveExportExcelVO> getExportList(AskLeavePageReqVO reqVO);
+
+    /**
+     * 出院扫码:根据 elderId 查询外出记录中外出的时间为空的记录,更新外出时间为当前时间
+     *
+     * @param elderId 老人ID
+     */
+    void scanOut(Long elderId);
+
+    /**
+     * 回院扫码:根据 elderId 查询外出记录中实际返回时间为空的记录,更新实际返回时间为当前时间
+     *
+     * @param elderId 老人ID
+     */
+    void scanReturn(Long elderId);
 }

+ 33 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyAskLeaveServiceImpl.java

@@ -120,6 +120,8 @@ public class ElderlyAskLeaveServiceImpl implements ElderlyAskLeaveService {
         if (createReqVO.getLeaveStatus() == null) {
             createReqVO.setLeaveStatus(ElderlyAskLeaveDO.LEAVE_STATUS_APPROVED);
         }
+        // 该接口为院区申请
+        createReqVO.setRequestType(ElderlyAskLeaveDO.REQUEST_TYPE_INSTITUTION);
         elderlyAskLeaveMapper.insert(createReqVO);
     }
 
@@ -920,4 +922,35 @@ public class ElderlyAskLeaveServiceImpl implements ElderlyAskLeaveService {
         elderlyAskLeaveDailyExpenseRelationDO.setTenantId(TenantContextHolder.getTenantId());
         elderlyAskLeaveDailyExpenseRelationMapper.insert(elderlyAskLeaveDailyExpenseRelationDO);
     }
+
+    @Override
+    public void scanOut(Long elderId) {
+        ElderlyAskLeaveDO askLeave = elderlyAskLeaveMapper.selectOne(
+                new LambdaQueryWrapperX<ElderlyAskLeaveDO>()
+                        .eq(ElderlyAskLeaveDO::getElderId, elderId)
+                        .isNull(ElderlyAskLeaveDO::getOutDate)
+                        .orderByDesc(ElderlyAskLeaveDO::getId)
+                        .last("LIMIT 1"));
+        if (askLeave == null) {
+            throw exception(ErrorCodeConstants.ELDERLY_LEAVE_SCAN_OUT_NOT_FOUND);
+        }
+        askLeave.setOutDate(new Date());
+        elderlyAskLeaveMapper.updateById(askLeave);
+    }
+
+    @Override
+    public void scanReturn(Long elderId) {
+        ElderlyAskLeaveDO askLeave = elderlyAskLeaveMapper.selectOne(
+                new LambdaQueryWrapperX<ElderlyAskLeaveDO>()
+                        .eq(ElderlyAskLeaveDO::getElderId, elderId)
+                        .isNotNull(ElderlyAskLeaveDO::getOutDate)
+                        .isNull(ElderlyAskLeaveDO::getUpdateDate)
+                        .orderByDesc(ElderlyAskLeaveDO::getId)
+                        .last("LIMIT 1"));
+        if (askLeave == null) {
+            throw exception(ErrorCodeConstants.ELDERLY_LEAVE_SCAN_RETURN_NOT_FOUND);
+        }
+        askLeave.setUpdateDate(new Date());
+        elderlyAskLeaveMapper.updateById(askLeave);
+    }
 }

+ 304 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/NurseChangeRecordServiceImpl.java

@@ -13,13 +13,17 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.module.system.controller.admin.biz.vo.BedChangeRecordOneReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.biz.vo.BedChangeElderlyRespVO;
 import cn.iocoder.yudao.module.system.controller.admin.biz.vo.*;
 import cn.iocoder.yudao.module.system.dal.dataobject.biz.*;
 import cn.iocoder.yudao.module.system.dal.mysql.biz.*;
+import cn.iocoder.yudao.module.system.enums.change.BusinessConstants;
+import cn.iocoder.yudao.module.system.enums.change.ChangeConstants;
 import cn.iocoder.yudao.module.system.enums.common.BooleanEnum;
+import cn.iocoder.yudao.module.system.util.BizUtil;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -29,12 +33,24 @@ import org.springframework.validation.annotation.Validated;
 import javax.annotation.Resource;
 
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.LocalDate;
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAdjusters;
 import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Collectors;
 
+import org.apache.commons.lang3.StringUtils;
+
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exceptionCustomMsg;
 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.BED_CHANGE_RECORD_NOT_EXISTS;
@@ -73,6 +89,21 @@ public class NurseChangeRecordServiceImpl implements ElderlyChangeRecordService
     @Resource
     private BedSwapRecordMapper bedSwapRecordMapper;
 
+    @Resource
+    private DailyExpensesMapper dailyExpensesMapper;
+
+    @Resource
+    private ExpenseOrderMapper expenseOrderMapper;
+
+    @Resource
+    private ExpenseOrderItemMapper expenseOrderItemMapper;
+
+    @Resource
+    private SysChargeCategoryMapper chargeCategoryMapper;
+
+    @Autowired
+    private BizUtil bizUtil;
+
     @Override
     public Long createBedChangeRecord(BedChangeRecordSaveReqVO createReqVO) {
         // 插入
@@ -439,6 +470,279 @@ public class NurseChangeRecordServiceImpl implements ElderlyChangeRecordService
                 || originalActual.compareTo(newActual) != 0;
 
         expenseItemMapper.insertBatch(newExpenseItemList);
+
+        // 费用变更后,对未缴费账单进行退补(参考 BpmElderlyChangeApiImpl#generateBill)
+        String itemCategory = getNameRecursive(originalItem.getItemCategoryId(), "");
+        processRefundSupplement(elderId, reqVO.getStartDate(), originalActual, newActual,
+                itemCategory, newCharge.getChargeName(), newCharge.getId());
+
         return amountChanged;
     }
+
+    /**
+     * 床位对调费用变更后,对未缴费的月度账单进行退补。
+     * 计算变更前后月费用的差额(按变更日到费用截止日的天数折算),生成日常费用记录,
+     * 并对未缴费、未锁定的账单追加账单明细项、同步更新账单应收金额。
+     * 逻辑参照 cn.iocoder.yudao.module.system.api.bpm.BpmElderlyChangeApiImpl#generateBill
+     *
+     * @param elderId       长者ID
+     * @param changeDate    变更生效日期(即对调开始日期)
+     * @param currentExpense 变更前月费用(原费用项实际金额)
+     * @param changeExpense  变更后月费用(新费用项实际金额)
+     * @param itemCategory   费用分类名称
+     * @param itemName       费用项名称
+     * @param overheadChargeId 收费项ID
+     */
+    private void processRefundSupplement(Long elderId, LocalDate changeDate, BigDecimal currentExpense,
+                                         BigDecimal changeExpense, String itemCategory, String itemName,
+                                         Long overheadChargeId) {
+        if (currentExpense == null) {
+            currentExpense = BigDecimal.ZERO;
+        }
+        if (changeExpense == null) {
+            changeExpense = BigDecimal.ZERO;
+        }
+        // 1. 计算退补金额(按变更日到费用截止日的剩余天数折算)
+        BigDecimal daysInCurrentMonth = BigDecimal.valueOf(changeDate.lengthOfMonth());
+        LocalDate firstDayOfMonth = changeDate.withDayOfMonth(1);
+        BigDecimal currentDays = BigDecimal.valueOf(changeDate.toEpochDay() - firstDayOfMonth.toEpochDay());
+        LocalDate endDay = bizUtil.getEndDateWithExpireDate(elderId, null,
+                firstDayOfMonth.with(TemporalAdjusters.lastDayOfMonth()));
+        // 新价格使用天数
+        BigDecimal changeDays = new BigDecimal(Math.abs(ChronoUnit.DAYS.between(endDay, changeDate)) + 1);
+
+        BigDecimal finalAmount;
+        if (0 == currentExpense.setScale(2).compareTo(changeExpense.setScale(2))) {
+            finalAmount = BigDecimal.ZERO;
+        } else if (daysInCurrentMonth.equals(changeDays)) {
+            finalAmount = changeExpense.subtract(currentExpense);
+        } else {
+            // 原价格每天价格
+            BigDecimal currentDayAmount = currentExpense.divide(daysInCurrentMonth, 8, RoundingMode.HALF_UP);
+            // 新价格每天价格
+            BigDecimal changeDayAmount = changeExpense.divide(daysInCurrentMonth, 8, RoundingMode.HALF_UP);
+            // 原价格剩余费用 = 改变天数 * 原价格
+            BigDecimal currentFinalAmount = changeDays.multiply(currentDayAmount);
+            // 新价格剩余费用 = 改变天数 * 新价格
+            BigDecimal changeFinalAmount = changeDays.multiply(changeDayAmount);
+            // 最终价格 = 新 - 原(正为补收,负为退费)
+            finalAmount = changeFinalAmount.subtract(currentFinalAmount);
+        }
+        if (finalAmount.compareTo(BigDecimal.ZERO) == 0) {
+            return;
+        }
+
+        DateTimeFormatter billingMonthFormatter = DateTimeFormatter.ofPattern("yyyy-MM");
+        String changeBillingMonth = changeDate.format(billingMonthFormatter);
+        YearMonth changeYearMonth = YearMonth.parse(changeBillingMonth, billingMonthFormatter);
+        Date currentDate = new Date();
+        BigDecimal dailyExpensesDOAmount = finalAmount;
+        String remark = String.format(ChangeConstants.BED_SWAP_DAILY_PAY_REMARK, currentExpense.setScale(2),
+                currentExpense.setScale(2), changeExpense.setScale(2), currentDays, changeDays);
+
+        // 2. 查询变更生效月及之后已生成的账单
+        List<ExpenseOrderDO> generateExpenseOrder = expenseOrderMapper.selectList(new LambdaQueryWrapperX<ExpenseOrderDO>()
+                .eq(ExpenseOrderDO::getElderId, elderId)
+                .ge(ExpenseOrderDO::getBillingMonth, changeBillingMonth)
+                .ne(ExpenseOrderDO::getType, 4));
+        if (generateExpenseOrder.size() > 0) {
+            List<String> targetBillingMonths = new ArrayList<>(generateExpenseOrder.size());
+            Set<String> targetBillingMonthSet = new HashSet<>(generateExpenseOrder.size());
+            for (ExpenseOrderDO e : generateExpenseOrder) {
+                String targetBillingMonth = resolveAttributionBillMonth(e.getBillingMonth(), changeYearMonth, null);
+                targetBillingMonths.add(targetBillingMonth);
+                targetBillingMonthSet.add(targetBillingMonth);
+            }
+
+            Map<String, ExpenseOrderDO> targetOrderMap = new HashMap<>();
+            if (CollectionUtil.isNotEmpty(targetBillingMonthSet)) {
+                List<ExpenseOrderDO> targetOrders = expenseOrderMapper.selectList(new LambdaQueryWrapperX<ExpenseOrderDO>()
+                        .eq(ExpenseOrderDO::getElderId, elderId)
+                        .ne(ExpenseOrderDO::getType, 4)
+                        .in(ExpenseOrderDO::getBillingMonth, targetBillingMonthSet)
+                        .orderByDesc(ExpenseOrderDO::getCreatedTime));
+                for (ExpenseOrderDO targetOrder : targetOrders) {
+                    targetOrderMap.putIfAbsent(targetOrder.getBillingMonth(), targetOrder);
+                }
+            }
+
+            List<DailyExpensesDO> currentDailyExpensesDOList = new ArrayList<>(generateExpenseOrder.size());
+            for (ExpenseOrderDO e : generateExpenseOrder) {
+                DailyExpensesDO dailyExpensesDO = new DailyExpensesDO();
+                dailyExpensesDO.setElderId(elderId);
+                dailyExpensesDO.setItemId(overheadChargeId);
+                dailyExpensesDO.setItemCategory(itemCategory);
+                dailyExpensesDO.setItemName(itemName);
+                dailyExpensesDO.setType(2);
+                dailyExpensesDO.setPrice(changeExpense.setScale(2));
+                if (changeBillingMonth.equals(e.getBillingMonth())) {
+                    dailyExpensesDO.setAmount(dailyExpensesDOAmount);
+                    dailyExpensesDO.setStartDate(changeDate);
+                    dailyExpensesDO.setEndDate(endDay);
+                    dailyExpensesDO.setRemarks(remark);
+                } else {
+                    dailyExpensesDO.setAmount(changeExpense.subtract(currentExpense));
+                    YearMonth yearMonth = YearMonth.parse(e.getBillingMonth());
+                    dailyExpensesDO.setStartDate(yearMonth.atDay(1));
+                    dailyExpensesDO.setEndDate(yearMonth.atEndOfMonth());
+                    dailyExpensesDO.setRemarks(String.format(ChangeConstants.BED_SWAP_DAILY_PAY_REMARK,
+                            currentExpense.setScale(2), currentExpense.setScale(2), changeExpense.setScale(2),
+                            0, yearMonth.lengthOfMonth()));
+                }
+                dailyExpensesDO.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
+                dailyExpensesDO.setCreatedTime(currentDate);
+                dailyExpensesDO.setAccountingTime(currentDate);
+                dailyExpensesDO.setCount(1);
+                String targetBillingMonth = resolveAttributionBillMonth(e.getBillingMonth(), changeYearMonth, null);
+                dailyExpensesDO.setAttributionBillTime(targetBillingMonth);
+                ExpenseOrderDO targetExpenseOrder = targetOrderMap.get(targetBillingMonth);
+                if (targetExpenseOrder != null && !targetExpenseOrder.getPayStatus().equals(BooleanEnum.TRUE.getValue())
+                        && !targetExpenseOrder.getIsLock()) {
+                    dailyExpensesDO.setIsGenerateBill(BooleanEnum.TRUE.getValue());
+                } else {
+                    dailyExpensesDO.setIsGenerateBill(BooleanEnum.FALSE.getValue());
+                }
+                currentDailyExpensesDOList.add(dailyExpensesDO);
+            }
+            dailyExpensesMapper.insertBatch(currentDailyExpensesDOList);
+
+            // 为未缴费、未锁定的账单创建账单明细项并更新账单金额
+            for (int i = 0; i < generateExpenseOrder.size(); i++) {
+                DailyExpensesDO dailyExpensesDO = currentDailyExpensesDOList.get(i);
+                ExpenseOrderDO targetExpenseOrder = targetOrderMap.get(targetBillingMonths.get(i));
+                if (targetExpenseOrder != null && !targetExpenseOrder.getPayStatus().equals(BooleanEnum.TRUE.getValue())
+                        && !targetExpenseOrder.getIsLock()) {
+                    ExpenseOrderItemDO expenseOrderItemDO = new ExpenseOrderItemDO();
+                    expenseOrderItemDO.setExpenseOrderId(targetExpenseOrder.getId());
+                    expenseOrderItemDO.setSourceExpenseItemId(dailyExpensesDO.getId());
+                    expenseOrderItemDO.setExpenseSource(BusinessConstants.DAILY_EXPENSES);
+                    expenseOrderItemDO.setItemCategoryName(dailyExpensesDO.getItemCategory());
+                    expenseOrderItemDO.setItemName(dailyExpensesDO.getItemName());
+                    expenseOrderItemDO.setPrice(dailyExpensesDO.getPrice());
+                    expenseOrderItemDO.setActualPrice(dailyExpensesDO.getPrice());
+                    expenseOrderItemDO.setTotalAmount(dailyExpensesDO.getAmount());
+                    expenseOrderItemDO.setRoundAmount(dailyExpensesDO.getAmount().setScale(0, RoundingMode.HALF_UP));
+                    expenseOrderItemDO.setRoundTwoDecimalAmount(dailyExpensesDO.getAmount().setScale(2, RoundingMode.HALF_UP));
+                    expenseOrderItemDO.setCount(dailyExpensesDO.getCount());
+                    expenseOrderItemDO.setType(dailyExpensesDO.getType());
+                    expenseOrderItemDO.setStartDate(dailyExpensesDO.getStartDate());
+                    expenseOrderItemDO.setEndDate(dailyExpensesDO.getEndDate());
+                    expenseOrderItemDO.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
+                    expenseOrderItemDO.setCreatedTime(currentDate);
+                    expenseOrderItemDO.setTenantId(targetExpenseOrder.getTenantId());
+                    expenseOrderItemDO.setPayStatus(0);
+                    expenseOrderItemMapper.insert(expenseOrderItemDO);
+
+                    // 更新账单金额
+                    BigDecimal currentAmount = targetExpenseOrder.getActualAmount() != null
+                            ? targetExpenseOrder.getActualAmount() : BigDecimal.ZERO;
+                    BigDecimal newAmount = currentAmount.add(expenseOrderItemDO.getTotalAmount());
+                    ExpenseOrderDO updateExpenseOrderDO = new ExpenseOrderDO();
+                    updateExpenseOrderDO.setId(targetExpenseOrder.getId());
+                    updateExpenseOrderDO.setActualAmount(newAmount);
+                    expenseOrderMapper.updateById(updateExpenseOrderDO);
+                    targetExpenseOrder.setActualAmount(newAmount);
+                }
+            }
+        }
+
+        // 3. 生效当月若无未缴费账单,则生成日常费用(后续由账单生成流程并入账单)
+        ExpenseOrderDO expenseOrder = expenseOrderMapper.selectOne(new LambdaQueryWrapperX<ExpenseOrderDO>()
+                .eq(ExpenseOrderDO::getElderId, elderId)
+                .eq(ExpenseOrderDO::getBillingMonth, changeBillingMonth)
+                .eq(ExpenseOrderDO::getPayStatus, BooleanEnum.FALSE.getValue())
+                .ne(ExpenseOrderDO::getType, 4));
+        if (expenseOrder == null) {
+            String targetBillingMonth = resolveAttributionBillMonth(changeBillingMonth, changeYearMonth, null);
+            ExpenseOrderDO targetExpenseOrder = expenseOrderMapper.selectOne(new LambdaQueryWrapperX<ExpenseOrderDO>()
+                    .eq(ExpenseOrderDO::getElderId, elderId)
+                    .eq(ExpenseOrderDO::getBillingMonth, targetBillingMonth));
+            DailyExpensesDO dailyExpensesDO = new DailyExpensesDO();
+            dailyExpensesDO.setElderId(elderId);
+            dailyExpensesDO.setItemId(overheadChargeId);
+            dailyExpensesDO.setItemCategory(itemCategory);
+            dailyExpensesDO.setItemName(itemName);
+            dailyExpensesDO.setType(2);
+            dailyExpensesDO.setPrice(changeExpense.setScale(2));
+            dailyExpensesDO.setAmount(dailyExpensesDOAmount);
+            dailyExpensesDO.setRemarks(remark);
+            dailyExpensesDO.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
+            dailyExpensesDO.setCreatedTime(currentDate);
+            dailyExpensesDO.setStartDate(changeDate);
+            dailyExpensesDO.setEndDate(endDay);
+            dailyExpensesDO.setAccountingTime(currentDate);
+            dailyExpensesDO.setCount(1);
+            dailyExpensesDO.setAttributionBillTime(targetBillingMonth);
+            if (targetExpenseOrder != null && !targetExpenseOrder.getPayStatus().equals(BooleanEnum.TRUE.getValue())
+                    && !targetExpenseOrder.getIsLock()) {
+                dailyExpensesDO.setIsGenerateBill(BooleanEnum.TRUE.getValue());
+            } else {
+                dailyExpensesDO.setIsGenerateBill(BooleanEnum.FALSE.getValue());
+            }
+            dailyExpensesMapper.insert(dailyExpensesDO);
+            if (dailyExpensesDO.getIsGenerateBill().equals(BooleanEnum.TRUE.getValue())) {
+                ExpenseOrderItemDO expenseOrderItemDO = new ExpenseOrderItemDO();
+                expenseOrderItemDO.setExpenseOrderId(targetExpenseOrder.getId());
+                expenseOrderItemDO.setSourceExpenseItemId(dailyExpensesDO.getId());
+                expenseOrderItemDO.setExpenseSource(BusinessConstants.DAILY_EXPENSES);
+                expenseOrderItemDO.setItemCategoryName(dailyExpensesDO.getItemCategory());
+                expenseOrderItemDO.setItemName(dailyExpensesDO.getItemName());
+                expenseOrderItemDO.setPrice(dailyExpensesDO.getPrice());
+                expenseOrderItemDO.setActualPrice(dailyExpensesDO.getPrice());
+                expenseOrderItemDO.setTotalAmount(dailyExpensesDO.getAmount());
+                expenseOrderItemDO.setRoundAmount(dailyExpensesDO.getAmount().setScale(0, RoundingMode.HALF_UP));
+                expenseOrderItemDO.setRoundTwoDecimalAmount(dailyExpensesDO.getAmount().setScale(2, RoundingMode.HALF_UP));
+                expenseOrderItemDO.setCount(dailyExpensesDO.getCount());
+                expenseOrderItemDO.setType(dailyExpensesDO.getType());
+                expenseOrderItemDO.setStartDate(dailyExpensesDO.getStartDate());
+                expenseOrderItemDO.setEndDate(dailyExpensesDO.getEndDate());
+                expenseOrderItemDO.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
+                expenseOrderItemDO.setCreatedTime(currentDate);
+                expenseOrderItemDO.setTenantId(targetExpenseOrder.getTenantId());
+                expenseOrderItemDO.setPayStatus(0);
+                expenseOrderItemMapper.insert(expenseOrderItemDO);
+
+                BigDecimal currentAmount = targetExpenseOrder.getActualAmount() != null
+                        ? targetExpenseOrder.getActualAmount() : BigDecimal.ZERO;
+                BigDecimal newAmount = currentAmount.add(expenseOrderItemDO.getTotalAmount());
+                expenseOrderMapper.updateById(new ExpenseOrderDO().setId(targetExpenseOrder.getId()).setActualAmount(newAmount));
+            }
+        }
+    }
+
+    /**
+     * 递归获取费用分类全路径名称(与 BpmElderlyChangeApiImpl#getNameRecursive 一致)
+     */
+    private String getNameRecursive(Long itemCategoryId, String name) {
+        SysChargeCategoryDO sysChargeCategoryDO = chargeCategoryMapper.selectById(itemCategoryId);
+        if (null == sysChargeCategoryDO || null == sysChargeCategoryDO.getParentId()) {
+            String[] split = name.split("/");
+            if (null != split && split.length > 2) {
+                return split[1] + "/" + split[2];
+            }
+            if (null != split && split.length > 1) {
+                return split[0] + "/" + split[1];
+            }
+            return name;
+        }
+        String returnName;
+        if (StringUtils.isEmpty(name)) {
+            returnName = sysChargeCategoryDO.getName();
+        } else {
+            returnName = sysChargeCategoryDO.getName() + "/" + name;
+        }
+        return getNameRecursive(sysChargeCategoryDO.getParentId(), returnName);
+    }
+
+    /**
+     * 解析日常费用归属账单月(床位对调不涉及滞后月,原样返回)
+     */
+    private String resolveAttributionBillMonth(String sourceBillingMonth, YearMonth changeYearMonth, YearMonth hysteresisYearMonth) {
+        if (hysteresisYearMonth == null || changeYearMonth == null || StringUtils.isBlank(sourceBillingMonth)) {
+            return sourceBillingMonth;
+        }
+        YearMonth sourceYearMonth = YearMonth.parse(sourceBillingMonth);
+        long monthOffset = ChronoUnit.MONTHS.between(changeYearMonth, sourceYearMonth);
+        return hysteresisYearMonth.plusMonths(monthOffset).toString();
+    }
 }

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/RefundSettlementOrderService.java

@@ -13,7 +13,7 @@ import cn.iocoder.yudao.module.system.dal.dataobject.biz.RefundSettlementOrderDO
 public interface RefundSettlementOrderService {
 
 
-    PageResult<RefundSettlementOrderDO> getRefundSettlementOrderPage(RefundSettlementOrderPageReqVO pageReqVO);
+    PageResult<RefundSettlementOrderPageRespVO> getRefundSettlementOrderPage(RefundSettlementOrderPageReqVO pageReqVO);
 
     RefundSettlementOrderRespVO getRefundSettlementOrder(Long id);
     

+ 10 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/RefundSettlementOrderServiceImpl.java

@@ -111,15 +111,23 @@ public class RefundSettlementOrderServiceImpl implements RefundSettlementOrderSe
     private BizUtil bizUtil;
 
     @Override
-    public PageResult<RefundSettlementOrderDO> getRefundSettlementOrderPage(RefundSettlementOrderPageReqVO pageReqVO) {
+    public PageResult<RefundSettlementOrderPageRespVO> getRefundSettlementOrderPage(RefundSettlementOrderPageReqVO pageReqVO) {
         Page<RefundSettlementOrderDO> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
         List<RefundSettlementOrderDO> list = refundSettlementOrderMapper.getRefundSettlementOrderPage(page, pageReqVO);
+        List<RefundSettlementOrderPageRespVO> respList = new ArrayList<>(list.size());
         for (RefundSettlementOrderDO refundSettlementOrderDO : list) {
             refundSettlementOrderDO.setBedName(buildService.getFullBedName(refundSettlementOrderDO.getElderId()));
             TenantDO tenantDO = tenantMapper.selectById(refundSettlementOrderDO.getTenantId());
             refundSettlementOrderDO.setTenantName(tenantDO == null ? null : tenantDO.getName());
+            RefundSettlementOrderPageRespVO respVO = cn.iocoder.yudao.framework.common.util.object.BeanUtils.toBean(refundSettlementOrderDO, RefundSettlementOrderPageRespVO.class);
+            ElderlyRetreatRecordDO elderlyRetreatRecord = elderlyRetreatRecordMapper.selectById(refundSettlementOrderDO.getBusinessId());
+            if (elderlyRetreatRecord != null) {
+                respVO.setRetreatDate(elderlyRetreatRecord.getRetreatDate());
+                respVO.setCostDeadlineDate(elderlyRetreatRecord.getCostDeadlineDate());
+            }
+            respList.add(respVO);
         }
-        return new PageResult<>(list, page.getTotal());
+        return new PageResult<>(respList, page.getTotal());
     }
 
     @Override

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/miniapp/JiashuVisitAppointmentService.java

@@ -14,6 +14,6 @@ public interface JiashuVisitAppointmentService {
      *
      * @return 当日 / 当周 / 当月的预约数量
      */
-    VisitAppointmentCountRespVO getVisitCount();
+    VisitAppointmentCountRespVO getVisitCount(Long tenantId);
 
 }

+ 10 - 10
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/miniapp/JiashuVisitAppointmentServiceImpl.java

@@ -23,40 +23,40 @@ public class JiashuVisitAppointmentServiceImpl implements JiashuVisitAppointment
     private JiashuVisitAppointmentMapper jiashuVisitAppointmentMapper;
 
     @Override
-    public VisitAppointmentCountRespVO getVisitCount() {
+    public VisitAppointmentCountRespVO getVisitCount(Long tenantId) {
         VisitAppointmentCountRespVO respVO = new VisitAppointmentCountRespVO();
-        respVO.setTodayCount(getTodayCount());
-        respVO.setWeekCount(getWeekCount());
-        respVO.setMonthCount(getMonthCount());
+        respVO.setTodayCount(getTodayCount(tenantId));
+        respVO.setWeekCount(getWeekCount(tenantId));
+        respVO.setMonthCount(getMonthCount(tenantId));
         return respVO;
     }
 
     /**
      * 当日预约参观数量
      */
-    private Long getTodayCount() {
+    private Long getTodayCount(Long tenantId) {
         LocalDate today = LocalDate.now();
-        return jiashuVisitAppointmentMapper.countByVisitDateRange(today, today);
+        return jiashuVisitAppointmentMapper.countByVisitDateRange(today, today,tenantId);
     }
 
     /**
      * 当周(周一至周日)预约参观数量
      */
-    private Long getWeekCount() {
+    private Long getWeekCount(Long tenantId) {
         LocalDate now = LocalDate.now();
         LocalDate monday = now.with(DayOfWeek.MONDAY);
         LocalDate sunday = now.with(DayOfWeek.SUNDAY);
-        return jiashuVisitAppointmentMapper.countByVisitDateRange(monday, sunday);
+        return jiashuVisitAppointmentMapper.countByVisitDateRange(monday, sunday,tenantId);
     }
 
     /**
      * 当月(1号至月末)预约参观数量
      */
-    private Long getMonthCount() {
+    private Long getMonthCount(Long tenantId) {
         LocalDate now = LocalDate.now();
         LocalDate firstDay = now.withDayOfMonth(1);
         LocalDate lastDay = now.withDayOfMonth(now.lengthOfMonth());
-        return jiashuVisitAppointmentMapper.countByVisitDateRange(firstDay, lastDay);
+        return jiashuVisitAppointmentMapper.countByVisitDateRange(firstDay, lastDay,tenantId);
     }
 
 }

+ 3 - 0
yudao-server/src/main/resources/application.yaml

@@ -252,6 +252,9 @@ yudao:
       - /admin-api/elderly/askLeave/createBpm
       - /admin-api/elderly/askLeave/findPage
       - /admin-api/elderly/outboundRefundConfig/findList
+      - /admin-api/elderly/askLeave/scanOut
+      - /admin-api/elderly/askLeave/scanReturn
+      - /admin-api/system/appointment-quota/query
 #      - /admin-api/bpm/check-in-wait/create
     ignore-tables:
       - system_tenant