3 Commit-ok a0a7efe5ca ... d8e30f1149

Szerző SHA1 Üzenet Dátum
  liangwenxuan d8e30f1149 补充提交 1 hete
  liangwenxuan a4e64ee11c 补充提交 1 hete
  liangwenxuan b99739613d 新增 1 hete
24 módosított fájl, 1044 hozzáadás és 11 törlés
  1. 27 0
      sql/mysql/elderly_nursing_record.sql
  2. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/BpmElderlyController.java
  3. 35 3
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java
  4. 0 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceCopyController.java
  5. 12 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyService.java
  6. 46 4
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImpl.java
  7. 4 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
  8. 84 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/ElderlyNursingRecordController.java
  9. 49 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/FeeItemChangeRecordController.java
  10. 109 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/FeeItemChangeDetailRespVO.java
  11. 29 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/nursingrecord/NursingRecordPageReqVO.java
  12. 94 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/nursingrecord/NursingRecordRespVO.java
  13. 97 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/nursingrecord/NursingRecordSaveReqVO.java
  14. 3 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/pay/CcbWxUnifiedOrderController.java
  15. 83 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/biz/ElderlyNursingRecordDO.java
  16. 2 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/ccb/MerchantRecordDO.java
  17. 26 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/biz/ElderlyChangeRecordMapper.java
  18. 28 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/biz/ElderlyNursingRecordMapper.java
  19. 13 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyChangeRecordService.java
  20. 50 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyNursingRecordService.java
  21. 103 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyNursingRecordServiceImpl.java
  22. 19 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/NurseChangeRecordServiceImpl.java
  23. 93 0
      yudao-module-system/yudao-module-system-biz/src/main/resources/mapper/ElderlyChangeRecordMapper.xml
  24. 37 0
      yudao-module-system/yudao-module-system-biz/src/main/resources/mapper/ElderlyNursingRecordMapper.xml

+ 27 - 0
sql/mysql/elderly_nursing_record.sql

@@ -0,0 +1,27 @@
+DROP TABLE IF EXISTS `elderly_nursing_record`;
+CREATE TABLE `elderly_nursing_record`  (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '机构ID(多租户)',
+  `elder_id` bigint(20) NOT NULL COMMENT '长者ID',
+  `elder_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '长者姓名(冗余)',
+  `elder_sex` tinyint(4) NULL DEFAULT NULL COMMENT '性别(冗余)1-男 2-女',
+  `bed_info` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '楼层房号床号(冗余,如 3栋-2楼-201-1床)',
+  `record_date` date NOT NULL COMMENT '记录日期',
+  `record_time` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '记录时间 HH:mm',
+  `temperature` decimal(4, 1) NULL DEFAULT NULL COMMENT '体温 ℃',
+  `blood_oxygen` smallint(6) NULL DEFAULT NULL COMMENT '血氧 Spo2 %',
+  `pulse` smallint(6) NULL DEFAULT NULL COMMENT '脉搏 次/分',
+  `respiration` smallint(6) NULL DEFAULT NULL COMMENT '呼吸 次/分',
+  `systolic_pressure` smallint(6) NULL DEFAULT NULL COMMENT '收缩压 mmHg',
+  `diastolic_pressure` smallint(6) NULL DEFAULT NULL COMMENT '舒张压 mmHg',
+  `observation` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '病情观察及护理',
+  `signer` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '签名(护理人员姓名)',
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人',
+  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新人',
+  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `idx_elder_date`(`elder_id`, `record_date`) USING BTREE,
+  INDEX `idx_tenant`(`tenant_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '护理记录单' ROW_FORMAT = DYNAMIC;

+ 1 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/business/BpmElderlyController.java

@@ -106,7 +106,7 @@ public class BpmElderlyController {
     }
 
     @PostMapping("/feeItemChangeCreate")
-    @PreAuthorize("@ss.hasAnyPermissions('fee-item-change:add', 'fee-item-change:relaunch')")
+//    @PreAuthorize("@ss.hasAnyPermissions('fee-item-change:add', 'fee-item-change:relaunch')")
     @Operation(summary = "创建费用项目变更")
     @TenantIgnore
     public CommonResult<Long> feeItemChangeCreate(@Valid @RequestBody FeeItemChangeRecordCreateReqVO createReqVO) {

+ 35 - 3
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java

@@ -13,6 +13,7 @@ import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.BedChangePageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.elderly.BpmElderlyInfoVO;
@@ -32,6 +33,7 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
 import cn.iocoder.yudao.module.bpm.service.business.BpmBusinessFormService;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
 import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
 import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
 import cn.iocoder.yudao.module.system.api.bpm.vo.*;
@@ -89,6 +91,9 @@ public class BpmProcessInstanceController {
     @Resource
     private BpmBusinessFormService businessFormService;
 
+    @Resource
+    private BpmProcessInstanceCopyService processInstanceCopyService;
+
     @GetMapping("/my-page")
     @Operation(summary = "获得我的实例分页列表", description = "在【我的流程】菜单中,进行调用")
     @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
@@ -181,9 +186,25 @@ public class BpmProcessInstanceController {
     @Parameter(name = "id", description = "流程实例的编号", required = true)
     @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
     public CommonResult<BpmProcessInstanceRespVO> getProcessInstance(@RequestParam("id") String id) {
+        // 1. 优先按当前登录用户所属租户读取
+        BpmProcessInstanceRespVO respVO = doGetProcessInstance(id);
+        if (respVO != null) {
+            return success(respVO);
+        }
+        // 2. 当前租户读不到:校验当前用户是否被该流程实例抄送。
+        //    集团用户被抄送时跨机构,其登录租户为集团,而流程数据属于机构租户
+        Long copyTenantId = processInstanceCopyService.getCopyTenantIdIgnoreTenant(getLoginUserId(), id);
+        if (copyTenantId == null) {
+            return success(null);
+        }
+        // 3. 确认存在抄送关系后,切换到流程所属机构租户读取
+        return success(TenantUtils.execute(copyTenantId, () -> doGetProcessInstance(id)));
+    }
+
+    private BpmProcessInstanceRespVO doGetProcessInstance(String id) {
         HistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(id);
         if (processInstance == null) {
-            return success(null);
+            return null;
         }
 
         // 拼接返回
@@ -198,8 +219,8 @@ public class BpmProcessInstanceController {
         if (startUser != null) {
             dept = deptApi.getDeptTenantIgnore(startUser.getDeptId());
         }
-        return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstance(processInstance,
-                processDefinition, processDefinitionInfo, bpmnXml, startUser, dept));
+        return BpmProcessInstanceConvert.INSTANCE.buildProcessInstance(processInstance,
+                processDefinition, processDefinitionInfo, bpmnXml, startUser, dept);
     }
 
     @DeleteMapping("/cancel-by-start-user")
@@ -225,6 +246,17 @@ public class BpmProcessInstanceController {
     @Parameter(name = "id", description = "流程实例的编号", required = true)
 //    @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
     public CommonResult<BpmApprovalDetailRespVO> getApprovalDetail(@Valid BpmApprovalDetailReqVO reqVO) {
+        // 集团用户被抄送时跨机构:流程定义等数据属于机构租户,直接用集团租户读取会查不到流程定义信息,
+        // 进而导致 getEndActivityNodeList 空指针,因此需先确认抄送关系再切换租户读取
+        if (reqVO.getProcessInstanceId() != null) {
+            Long copyTenantId = processInstanceCopyService.getCopyTenantIdIgnoreTenant(getLoginUserId(),
+                    reqVO.getProcessInstanceId());
+            // 仅在存在抄送关系、且流程不属于当前租户时才切换,避免越权读取其它机构数据
+            if (copyTenantId != null && !copyTenantId.equals(TenantContextHolder.getTenantId())) {
+                return success(TenantUtils.execute(copyTenantId,
+                        () -> processInstanceService.getApprovalDetail(getLoginUserId(), reqVO)));
+            }
+        }
         return success(processInstanceService.getApprovalDetail(getLoginUserId(), reqVO));
     }
 

+ 0 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceCopyController.java

@@ -51,7 +51,6 @@ public class BpmProcessInstanceCopyController {
 
     @GetMapping("/page")
     @Operation(summary = "获得抄送流程分页列表")
-    @PreAuthorize("@ss.hasPermission('bpm:process-instance-cc:query')")
     public CommonResult<PageResult<BpmProcessInstanceCopyRespVO>> getProcessInstanceCopyPage(
             @Valid BpmProcessInstanceCopyPageReqVO pageReqVO) {
         PageResult<BpmProcessInstanceCopyRespVO> pageResult = processInstanceCopyService.getProcessInstanceCopyPage(

+ 12 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyService.java

@@ -60,6 +60,18 @@ public interface BpmProcessInstanceCopyService {
     PageResult<BpmProcessInstanceCopyRespVO> getProcessInstanceCopyPage(Long userId,
                                                                         BpmProcessInstanceCopyPageReqVO pageReqVO);
 
+    /**
+     * 跨租户查询:当前用户是否被该流程实例抄送,若是则返回该抄送记录所属的租户(即流程发起方机构)
+     * <p>
+     * 用于集团用户查看机构流程的场景:集团用户在机构租户下没有账号,
+     * 流程数据全部属于机构租户,必须凭抄送关系确定目标租户后才能跨租户读取。
+     *
+     * @param userId            当前登录用户编号
+     * @param processInstanceId 流程实例编号
+     * @return 抄送记录所属租户编号;未被抄送时返回 null
+     */
+    Long getCopyTenantIdIgnoreTenant(Long userId, String processInstanceId);
+
     /**
      * 删除抄送流程
      *

+ 46 - 4
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImpl.java

@@ -34,6 +34,7 @@ import cn.iocoder.yudao.module.system.api.bpm.vo.ElderlyInfoReqVO;
 import cn.iocoder.yudao.module.system.enums.notice.NotieBusinessTypeEnum;
 import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.common.util.corp.CorpMsgUtil;
+import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
 import cn.iocoder.yudao.module.system.api.tenant.TenantApi;
 import cn.iocoder.yudao.module.system.api.tenant.TenantRespDTO;
 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -119,11 +120,13 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
         //Task task = taskService.getTask(taskId);
 
         // 2. 创建抄送流程
+        // 抄送记录必须写入【发起方租户】,否则集团用户跨机构查看时无法定位目标租户
+        Long tenantId = getStartUserTenantId(Long.valueOf(processInstance.getStartUserId()));
         List<BpmProcessInstanceCopyDO> copyList = convertList(userIds, userId -> new BpmProcessInstanceCopyDO()
                 .setUserId(userId).setReason(reason).setStartUserId(Long.valueOf(processInstance.getStartUserId()))
                 .setProcessInstanceId(processInstanceId).setProcessInstanceName(processInstance.getName())
                 .setCategory(processDefinition.getCategory()).setTaskId(taskId)
-                .setActivityId(activityId).setActivityName(activityName));
+                .setActivityId(activityId).setActivityName(activityName).setTenantId(tenantId));
         processInstanceCopyMapper.insertBatch(copyList);
         BpmBusinessFormDO businessFormDO = bpmBusinessFormMapper.selectOne(new LambdaQueryWrapper<BpmBusinessFormDO>()
                 .eq(BpmBusinessFormDO::getProcessInstanceId, processInstanceId));
@@ -241,7 +244,6 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
                         businessFormDO == null || businessFormDO.getStatus() == null
                                 ? "" : String.valueOf(businessFormDO.getStatus()),
                         copy.getUserId() == null ? "" : String.valueOf(copy.getUserId())));
-
                 WechatMessageSaveReq req = new WechatMessageSaveReq();
                 req.setTitle(CorpMsgUtil.TITLE_COPY);
                 req.setTenantName(tenantName);
@@ -267,6 +269,46 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
     }
 
     @Override
+    @TenantIgnore
+    public Long getCopyTenantIdIgnoreTenant(Long userId, String processInstanceId) {
+        // 跨租户查询:抄送记录落在流程发起方租户,集团用户在自己租户下查不到
+        List<BpmProcessInstanceCopyDO> list = processInstanceCopyMapper.selectList(
+                new LambdaQueryWrapper<BpmProcessInstanceCopyDO>()
+                        .eq(BpmProcessInstanceCopyDO::getUserId, userId)
+                        .eq(BpmProcessInstanceCopyDO::getProcessInstanceId, processInstanceId));
+        if (CollUtil.isEmpty(list)) {
+            return null;
+        }
+        BpmProcessInstanceCopyDO copy = list.get(0);
+        // 优先取记录自身租户;存量数据因历史落库缺陷为 0,回退到"发起人所属租户"
+        if (copy.getTenantId() != null && copy.getTenantId() > 0) {
+            return copy.getTenantId();
+        }
+        return getStartUserTenantId(copy.getStartUserId());
+    }
+
+    /**
+     * 查询流程发起人所属租户,即流程发起方机构。
+     * <p>
+     * 抄送记录必须写入发起方租户,集团用户被抄送后才能凭此定位到目标租户。
+     * 不能依赖 {@link TenantContextHolder}:节点抄送运行在 Flowable 事务内、审批抄送运行在 @Async 线程内,
+     * 租户上下文都可能取不到(这正是历史数据 tenant_id 全为 0 的原因),故统一以发起人所属租户为准。
+     *
+     * @param startUserId 流程发起人编号
+     * @return 发起方租户编号
+     */
+    private Long getStartUserTenantId(Long startUserId) {
+        if (ObjectUtil.isNotNull(startUserId)) {
+            AdminUserRespDTO startUser = adminUserApi.getUserTenantIgnore(startUserId);
+            if (startUser != null && startUser.getTenantId() != null) {
+                return startUser.getTenantId();
+            }
+        }
+        return TenantContextHolder.getTenantId();
+    }
+
+    @Override
+    @TenantIgnore // 抄送记录属于流程发起方租户,集团用户在自己租户下查不到,必须跨租户按 userId 查询
     public PageResult<BpmProcessInstanceCopyRespVO> getProcessInstanceCopyPage(Long userId,
                                                                            BpmProcessInstanceCopyPageReqVO pageReqVO) {
         Page<ElderlyRetreatRecordRespVO> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
@@ -299,8 +341,8 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
         }
 
         // 2. 创建抄送流程
-        // TODO
-        Long tenantId = TenantContextHolder.getTenantId();
+        // 抄送记录必须写入【发起方租户】:本方法为 @Async 异步执行,TenantContextHolder 取不到值
+        Long tenantId = getStartUserTenantId(Long.valueOf(processInstance.getStartUserId()));
         List<BpmProcessInstanceCopyDO> copyList = convertList(userIds, userId -> new BpmProcessInstanceCopyDO()
                 .setUserId(userId).setStartUserId(Long.valueOf(processInstance.getStartUserId()))
                 .setProcessInstanceId(processInstanceId).setProcessInstanceName(processInstance.getName())

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

@@ -353,4 +353,8 @@ public interface ErrorCodeConstants {
     // ========== 出院小结 1-008-026-000 ==========
     ErrorCode ELDERLY_DISCHARGE_ABSTRACT_NOT_EXISTS = new ErrorCode(1_008_026_000, "出院小结不存在");
     ErrorCode ELDERLY_DISCHARGE_USE_MEDICINE_NOT_EXISTS = new ErrorCode(1_008_026_001, "出院小结用药不存在");
+
+    // ========== 护理记录单 1-008-027-000 ==========
+    ErrorCode NURSING_RECORD_NOT_EXISTS = new ErrorCode(1_008_027_000, "护理记录单不存在");
+    ErrorCode NURSING_RECORD_BLOOD_PRESSURE_INVALID = new ErrorCode(1_008_027_001, "舒张压必须小于收缩压");
 }

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

@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.system.controller.admin.biz;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+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.nursingrecord.NursingRecordPageReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.nursingrecord.NursingRecordRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.nursingrecord.NursingRecordSaveReqVO;
+import cn.iocoder.yudao.module.system.service.biz.ElderlyNursingRecordService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+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;
+import javax.validation.Valid;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 护理记录单")
+@RestController
+@RequestMapping("/elderly/nursingRecord")
+@Validated
+public class ElderlyNursingRecordController {
+
+    @Resource
+    private ElderlyNursingRecordService nursingRecordService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建护理记录单")
+    public CommonResult<Long> create(@Valid @RequestBody NursingRecordSaveReqVO createReqVO) {
+        if (createReqVO.getTenantId() == null) {
+            createReqVO.setTenantId(TenantContextHolder.getTenantId());
+        }
+        return success(nursingRecordService.create(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新护理记录单")
+    @TenantIgnore
+    public CommonResult<Boolean> update(@Valid @RequestBody NursingRecordSaveReqVO updateReqVO) {
+        if (updateReqVO.getTenantId() == null) {
+            updateReqVO.setTenantId(TenantContextHolder.getTenantId());
+        }
+        nursingRecordService.update(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除护理记录单")
+    @Parameter(name = "id", description = "编号", required = true, example = "1")
+    @TenantIgnore
+    public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
+        nursingRecordService.delete(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得护理记录单")
+    @Parameter(name = "id", description = "编号", required = true, example = "1")
+    @TenantIgnore
+    public CommonResult<NursingRecordRespVO> get(@RequestParam("id") Long id) {
+        return success(nursingRecordService.get(id));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得护理记录单分页")
+    @TenantIgnore
+    public CommonResult<PageResult<NursingRecordRespVO>> page(@Valid NursingRecordPageReqVO pageReqVO) {
+        if (pageReqVO.getTenantIds() == null || pageReqVO.getTenantIds().length == 0) {
+            pageReqVO.setTenantIds(new Long[]{TenantContextHolder.getTenantId()});
+        }
+        return success(nursingRecordService.getPage(pageReqVO));
+    }
+}

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

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.system.controller.admin.biz;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.FeeItemChangeDetailRespVO;
+import cn.iocoder.yudao.module.system.service.biz.ElderlyChangeRecordService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+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;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+/**
+ * 管理后台 - 费用项目变更记录
+ * <p>
+ * 参考 {@link BedChangeRecordController#getBedChangeRecord} 实现
+ *
+ * @author 养老项目
+ */
+@Tag(name = "管理后台 - 费用项目变更记录")
+@RestController
+@RequestMapping("/elderly/feeItemChangeRecord")
+@Validated
+public class FeeItemChangeRecordController {
+
+    @Resource
+    private ElderlyChangeRecordService changeRecordService;
+
+    @GetMapping("/detail")
+    @Operation(summary = "获得费用项目变更记录详情")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('item-change:detail')")
+    @TenantIgnore
+    public CommonResult<FeeItemChangeDetailRespVO> getFeeItemChangeRecord(@RequestParam("id") Long id,
+                                                                          @RequestParam("status") Integer status,
+                                                                          @RequestParam("isDetail") Boolean isDetail) {
+        FeeItemChangeDetailRespVO feeItemChangeRecord = changeRecordService.getFeeItemDetail(id, status, isDetail);
+        return success(feeItemChangeRecord);
+    }
+
+}

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

@@ -0,0 +1,109 @@
+package cn.iocoder.yudao.module.system.controller.admin.biz.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
+
+/**
+ * 管理后台 - 费用项目变更详情 Response VO
+ * <p>
+ * 参考 {@link CateringChangeDetailRespVO} 实现
+ *
+ * @author 养老项目
+ */
+@Schema(description = "管理后台 - 费用项目变更详情 Response VO")
+@Data
+public class FeeItemChangeDetailRespVO {
+
+    @Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "14680")
+    private Long id;
+
+    @Schema(description = "长者id", example = "23841")
+    private Long elderlyId;
+
+    @Schema(description = "长者名称", example = "张三")
+    private String elderName;
+
+    @Schema(description = "证件号码")
+    private String idCard;
+
+    @Schema(description = "床位id", example = "20769")
+    private Long bedId;
+
+    @Schema(description = "床位号", example = "A栋3楼305-1")
+    private String bedName;
+
+    @Schema(description = "原费用项目id", example = "20769")
+    private Long originalId;
+
+    @Schema(description = "原费用项目名称", example = "水电费")
+    private String originalName;
+
+    @Schema(description = "新费用项目id", example = "16757")
+    private Long expectId;
+
+    @Schema(description = "新费用项目名称", example = "水电费套餐二")
+    private String expectName;
+
+    @Schema(description = "变更生效日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY)
+    private LocalDate changeDate;
+
+    @Schema(description = "收费项目id", example = "20769")
+    private Long overheadChargeId;
+
+    @Schema(description = "收费项目名称", example = "水电费")
+    private String categoryName;
+
+    @Schema(description = "收费项目名称")
+    private String originalItemName;
+
+    @Schema(description = "收费项目")
+    private String chargeName;
+
+    @Schema(description = "金额", example = "31331")
+    private BigDecimal amount;
+
+    @Schema(description = "变更前金额", example = "31331")
+    private BigDecimal originalAmount;
+
+    @Schema(description = "实际金额", example = "31331")
+    private BigDecimal actualAmount;
+
+    @Schema(description = "折扣金额", example = "31331")
+    private BigDecimal discountAmount;
+
+    @Schema(description = "折扣", example = "31331")
+    private String discount;
+
+    @Schema(description = "是否折扣", example = "7779")
+    private Integer isDiscount;
+
+    @Schema(description = "变更前是否折扣", example = "7779")
+    private Integer originalIsDiscount;
+
+    @Schema(description = "变更文件")
+    private String changeFiles;
+
+    @Schema(description = "变更原因", example = "不好")
+    private String reason;
+
+    @Schema(description = "创建时间")
+    private LocalDateTime createTime;
+
+    @Schema(description = "流程实例编号")
+    private String processInstanceId;
+
+    @Schema(description = "审批状态")
+    private Integer status;
+
+    @Schema(description = "发起流程的租户id")
+    private Long startTenantId;
+
+}

+ 29 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/nursingrecord/NursingRecordPageReqVO.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.system.controller.admin.biz.vo.nursingrecord;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 护理记录单分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class NursingRecordPageReqVO extends PageParam {
+
+    @Schema(description = "机构ID集合", example = "[1, 2]")
+    private Long[] tenantIds;
+
+    @Schema(description = "长者姓名(模糊)", example = "张桂英")
+    private String elderName;
+
+    @Schema(description = "记录日期区间", example = "['2026-09-01 00:00:00','2026-09-30 23:59:59']")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] recordDate;
+}

+ 94 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/nursingrecord/NursingRecordRespVO.java

@@ -0,0 +1,94 @@
+package cn.iocoder.yudao.module.system.controller.admin.biz.vo.nursingrecord;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
+
+@Schema(description = "管理后台 - 护理记录单 Response VO")
+@Data
+public class NursingRecordRespVO {
+
+    @Schema(description = "id", example = "1")
+    private Long id;
+
+    @Schema(description = "机构ID", example = "1")
+    private Long tenantId;
+
+    @Schema(description = "机构名称", example = "康养中心")
+    private String tenantName;
+
+    @Schema(description = "长者ID", example = "1001")
+    private Long elderId;
+
+    @Schema(description = "长者姓名", example = "张桂英")
+    private String elderName;
+
+    @Schema(description = "性别 1-男 2-女", example = "2")
+    private Integer elderSex;
+
+    @Schema(description = "楼层房号床号", example = "3栋-2楼-201-1床")
+    private String bedInfo;
+
+    @Schema(description = "记录日期", example = "2026-09-17")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
+    @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY, timezone = TIME_ZONE_DEFAULT)
+    private LocalDate recordDate;
+
+    @Schema(description = "记录时间 HH:mm", example = "08:00")
+    private String recordTime;
+
+    @Schema(description = "体温 ℃", example = "36.5")
+    private BigDecimal temperature;
+
+    @Schema(description = "血氧 Spo2 %", example = "97")
+    private Integer bloodOxygen;
+
+    @Schema(description = "脉搏 次/分", example = "76")
+    private Integer pulse;
+
+    @Schema(description = "呼吸 次/分", example = "18")
+    private Integer respiration;
+
+    @Schema(description = "收缩压 mmHg", example = "128")
+    private Integer systolicPressure;
+
+    @Schema(description = "舒张压 mmHg", example = "78")
+    private Integer diastolicPressure;
+
+    @Schema(description = "病情观察及护理")
+    private String observation;
+
+    @Schema(description = "签名(护理人员姓名)", example = "王护士")
+    private String signer;
+
+    @Schema(description = "创建人")
+    private String creator;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
+    private LocalDateTime createTime;
+
+    @Schema(description = "更新人")
+    private String updater;
+
+    @Schema(description = "更新时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
+    private LocalDateTime updateTime;
+
+    /**
+     * 根据床位实时计算的楼层房号床号,{@link #bedInfo} 为空时的兜底
+     */
+    @Schema(description = "楼层房号床号(实时计算)")
+    private String bedName;
+}

+ 97 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/nursingrecord/NursingRecordSaveReqVO.java

@@ -0,0 +1,97 @@
+package cn.iocoder.yudao.module.system.controller.admin.biz.vo.nursingrecord;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.validation.constraints.DecimalMax;
+import javax.validation.constraints.DecimalMin;
+import javax.validation.constraints.Digits;
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
+
+@Schema(description = "管理后台 - 护理记录单新增/修改 Request VO")
+@Data
+public class NursingRecordSaveReqVO {
+
+    @Schema(description = "id(修改时必传)", example = "1")
+    private Long id;
+
+    @Schema(description = "机构ID", example = "1")
+    private Long tenantId;
+
+    @Schema(description = "长者ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1001")
+    @NotNull(message = "长者ID不能为空")
+    private Long elderId;
+
+    @Schema(description = "长者姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张桂英")
+    @NotBlank(message = "长者姓名不能为空")
+    @Size(max = 64, message = "长者姓名最长 64 字")
+    private String elderName;
+
+    @Schema(description = "性别 1-男 2-女", example = "2")
+    private Integer elderSex;
+
+    @Schema(description = "楼层房号床号", example = "3栋-2楼-201-1床")
+    @Size(max = 128, message = "楼层房号床号最长 128 字")
+    private String bedInfo;
+
+    @Schema(description = "记录日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-09-17")
+    @NotNull(message = "记录日期不能为空")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
+    @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY, timezone = TIME_ZONE_DEFAULT)
+    private LocalDate recordDate;
+
+    @Schema(description = "记录时间 HH:mm", requiredMode = Schema.RequiredMode.REQUIRED, example = "08:00")
+    @NotBlank(message = "记录时间不能为空")
+    @Size(max = 8, message = "记录时间最长 8 字")
+    private String recordTime;
+
+    @Schema(description = "体温 ℃", example = "36.5")
+    @DecimalMin(value = "20", message = "体温不能小于 20")
+    @DecimalMax(value = "45", message = "体温不能大于 45")
+    @Digits(integer = 2, fraction = 1, message = "体温最多 1 位小数")
+    private BigDecimal temperature;
+
+    @Schema(description = "血氧 Spo2 %", example = "97")
+    @Min(value = 0, message = "血氧不能小于 0")
+    @Max(value = 100, message = "血氧不能大于 100")
+    private Integer bloodOxygen;
+
+    @Schema(description = "脉搏 次/分", example = "76")
+    @Min(value = 0, message = "脉搏不能小于 0")
+    @Max(value = 300, message = "脉搏不能大于 300")
+    private Integer pulse;
+
+    @Schema(description = "呼吸 次/分", example = "18")
+    @Min(value = 0, message = "呼吸不能小于 0")
+    @Max(value = 100, message = "呼吸不能大于 100")
+    private Integer respiration;
+
+    @Schema(description = "收缩压 mmHg", example = "128")
+    @Min(value = 0, message = "收缩压不能小于 0")
+    @Max(value = 300, message = "收缩压不能大于 300")
+    private Integer systolicPressure;
+
+    @Schema(description = "舒张压 mmHg", example = "78")
+    @Min(value = 0, message = "舒张压不能小于 0")
+    @Max(value = 300, message = "舒张压不能大于 300")
+    private Integer diastolicPressure;
+
+    @Schema(description = "病情观察及护理", example = "神志清楚,精神尚可,饮食正常。")
+    @Size(max = 500, message = "病情观察及护理最长 500 字")
+    private String observation;
+
+    @Schema(description = "签名(护理人员姓名)", example = "王护士")
+    @Size(max = 20, message = "签名最长 20 字")
+    private String signer;
+}

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

@@ -18,6 +18,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
@@ -190,7 +191,7 @@ public class CcbWxUnifiedOrderController {
         request.setPOSID(merchantRecordDO.getCounterNumber());
         request.setBRANCHID(CcbPayConstant.BRANCHID);
         request.setORDERID(orderId);
-        request.setPUB(CcbPayConstant.CCB_PUB); // 商户公钥后30位,参与关单MAC摘要
+        request.setPUB(StringUtils.right(merchantRecordDO.getSubPublic(),30)); // 商户公钥后30位,参与关单MAC摘要
         request.setSUB_APPID(subAppId); // 可选字段,可为null
         // 关单交易码,可通过参数临时覆盖(用于排查交易码是否正确,MAC校验失败可能由交易码不匹配导致)
         request.setTXCODE(txCode == null || txCode.isEmpty() ? CcbPayConstant.CLOSE_TXCODE : txCode);
@@ -315,7 +316,7 @@ public class CcbWxUnifiedOrderController {
             request.setPOSID(merchantRecordDO.getCounterNumber());
             request.setBRANCHID(CcbPayConstant.BRANCHID);
             request.setORDERID(expenseOrder.getBillOrderNumber());
-            request.setPUB(CcbPayConstant.CCB_PUB); // 商户公钥后30位,参与关单MAC摘要
+            request.setPUB(StringUtils.right(merchantRecordDO.getSubPublic(),30)); // 商户公钥后30位,参与关单MAC摘要
             request.setSUB_APPID(subAppId); // 可选字段,可为null
             request.setTXCODE(CcbPayConstant.CLOSE_TXCODE);
 

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

@@ -0,0 +1,83 @@
+package cn.iocoder.yudao.module.system.dal.dataobject.biz;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
+
+/**
+ * 护理记录单 DO
+ */
+@TableName("elderly_nursing_record")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ElderlyNursingRecordDO extends BaseDO {
+
+    @TableId
+    private Long id;
+
+    @Schema(description = "机构ID")
+    private Long tenantId;
+
+    @Schema(description = "长者ID")
+    private Long elderId;
+
+    @Schema(description = "长者姓名")
+    private String elderName;
+
+    @Schema(description = "性别 1-男 2-女")
+    private Integer elderSex;
+
+    @Schema(description = "楼层房号床号")
+    private String bedInfo;
+
+    @Schema(description = "记录日期")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
+    @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY, timezone = TIME_ZONE_DEFAULT)
+    private LocalDate recordDate;
+
+    @Schema(description = "记录时间 HH:mm")
+    private String recordTime;
+
+    @Schema(description = "体温 ℃")
+    private BigDecimal temperature;
+
+    @Schema(description = "血氧 Spo2 %")
+    private Integer bloodOxygen;
+
+    @Schema(description = "脉搏 次/分")
+    private Integer pulse;
+
+    @Schema(description = "呼吸 次/分")
+    private Integer respiration;
+
+    @Schema(description = "收缩压 mmHg")
+    private Integer systolicPressure;
+
+    @Schema(description = "舒张压 mmHg")
+    private Integer diastolicPressure;
+
+    @Schema(description = "病情观察及护理")
+    private String observation;
+
+    @Schema(description = "签名(护理人员姓名)")
+    private String signer;
+}

+ 2 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/ccb/MerchantRecordDO.java

@@ -31,4 +31,6 @@ public class MerchantRecordDO {
     private String account;
     // 机构id
     private Long tenantId;
+    // 子商户公钥
+    private String subPublic;
 }

+ 26 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/biz/ElderlyChangeRecordMapper.java

@@ -100,6 +100,32 @@ public interface ElderlyChangeRecordMapper extends BaseMapperX<ElderlyChangeReco
     CateringChangeDetailRespVO getCateringDetail(Long id);
     CateringChangeDetailRespVO createCateringChangeGetDetail(Long id);
 
+    /**
+     * 获得费用项目变更记录详情(isDetail = true,查看已有变更)
+     *
+     * @param id          变更记录编号
+     * @param currentFlag 是否当前最新,0-是,1-否
+     * @return 费用项目变更详情
+     */
+    FeeItemChangeDetailRespVO getFeeItemDetail(@Param("id") Long id, @Param("currentFlag") Integer currentFlag);
+
+    /**
+     * 获得费用项目变更记录详情(isDetail = false,重新提交时带出)
+     *
+     * @param id          变更记录编号
+     * @param currentFlag 是否当前最新,0-是,1-否
+     * @return 费用项目变更详情
+     */
+    FeeItemChangeDetailRespVO createFeeItemChangeGetDetail(@Param("id") Long id, @Param("currentFlag") Integer currentFlag);
+
+    /**
+     * 校验费用项目变更的业务表单是否存在
+     *
+     * @param id 变更记录编号
+     * @return 存在的数量
+     */
+    Integer checkFeeItemBpmBusinessFormById(Long id);
+
     TransferChangeDetailRespVO selectDetail(Long id);
 
     List<BedChangeElderlyRespVO> selectTransferElderlyPage(Page<ElderlyInfoDO> page, @Param("pageVO") ElderlyInfoPageReqVO pageVO);

+ 28 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/biz/ElderlyNursingRecordMapper.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.system.dal.mysql.biz;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.nursingrecord.NursingRecordPageReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.nursingrecord.NursingRecordRespVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.ElderlyNursingRecordDO;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 护理记录单 Mapper
+ */
+@Mapper
+public interface ElderlyNursingRecordMapper extends BaseMapperX<ElderlyNursingRecordDO> {
+
+    List<NursingRecordRespVO> selectPageWithBedInfo(Page<NursingRecordRespVO> page,
+                                                    @Param("reqVO") NursingRecordPageReqVO reqVO);
+
+    default PageResult<NursingRecordRespVO> selectPageWithBedInfo(NursingRecordPageReqVO reqVO) {
+        Page<NursingRecordRespVO> page = new Page<>(reqVO.getPageNo(), reqVO.getPageSize());
+        List<NursingRecordRespVO> list = selectPageWithBedInfo(page, reqVO);
+        return new PageResult<>(list, page.getTotal());
+    }
+}

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

@@ -68,6 +68,19 @@ public interface ElderlyChangeRecordService {
 
     CateringChangeDetailRespVO getCateringDetail(Long id, Boolean isDetail);
 
+    /**
+     * 获得费用项目变更记录详情
+     * <p>
+     * 参考 {@link #detail(Long, Integer, Boolean)} 实现:status 为 4(已取消)时会重新判定 currentFlag,
+     * isDetail 为 true 表示查看已有变更,false 表示重新提交时带出。
+     *
+     * @param id       变更记录编号
+     * @param status   审批状态
+     * @param isDetail 是否查看详情
+     * @return 费用项目变更详情
+     */
+    FeeItemChangeDetailRespVO getFeeItemDetail(Long id, Integer status, Boolean isDetail);
+
     /**
      * 床位对调
      *

+ 50 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyNursingRecordService.java

@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.system.service.biz;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.nursingrecord.NursingRecordPageReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.nursingrecord.NursingRecordRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.nursingrecord.NursingRecordSaveReqVO;
+
+/**
+ * 护理记录单 Service 接口
+ */
+public interface ElderlyNursingRecordService {
+
+    /**
+     * 新增护理记录
+     *
+     * @param createReqVO 新增参数
+     * @return 记录ID
+     */
+    Long create(NursingRecordSaveReqVO createReqVO);
+
+    /**
+     * 修改护理记录
+     *
+     * @param updateReqVO 修改参数
+     */
+    void update(NursingRecordSaveReqVO updateReqVO);
+
+    /**
+     * 删除护理记录
+     *
+     * @param id 记录ID
+     */
+    void delete(Long id);
+
+    /**
+     * 获得护理记录详情
+     *
+     * @param id 记录ID
+     * @return 护理记录
+     */
+    NursingRecordRespVO get(Long id);
+
+    /**
+     * 获得护理记录分页
+     *
+     * @param pageReqVO 分页参数
+     * @return 分页结果
+     */
+    PageResult<NursingRecordRespVO> getPage(NursingRecordPageReqVO pageReqVO);
+}

+ 103 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyNursingRecordServiceImpl.java

@@ -0,0 +1,103 @@
+package cn.iocoder.yudao.module.system.service.biz;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.nursingrecord.NursingRecordPageReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.nursingrecord.NursingRecordRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.nursingrecord.NursingRecordSaveReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.ElderlyNursingRecordDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.ElderlyNursingRecordMapper;
+import cn.iocoder.yudao.module.system.dal.mysql.tenant.TenantMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Objects;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.NURSING_RECORD_BLOOD_PRESSURE_INVALID;
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.NURSING_RECORD_NOT_EXISTS;
+
+/**
+ * 护理记录单 Service 实现类
+ */
+@Service
+@Validated
+public class ElderlyNursingRecordServiceImpl implements ElderlyNursingRecordService {
+
+    @Resource
+    private ElderlyNursingRecordMapper nursingRecordMapper;
+
+    @Resource
+    private TenantMapper tenantMapper;
+
+    @Resource
+    private BuildService buildService;
+
+    @Override
+    public Long create(NursingRecordSaveReqVO createReqVO) {
+        validateBloodPressure(createReqVO);
+        ElderlyNursingRecordDO record = BeanUtils.toBean(createReqVO, ElderlyNursingRecordDO.class);
+        nursingRecordMapper.insert(record);
+        return record.getId();
+    }
+
+    @Override
+    public void update(NursingRecordSaveReqVO updateReqVO) {
+        validateBloodPressure(updateReqVO);
+        validateExists(updateReqVO.getId());
+        ElderlyNursingRecordDO updateObj = BeanUtils.toBean(updateReqVO, ElderlyNursingRecordDO.class);
+        nursingRecordMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void delete(Long id) {
+        validateExists(id);
+        nursingRecordMapper.deleteById(id);
+    }
+
+    @Override
+    public NursingRecordRespVO get(Long id) {
+        ElderlyNursingRecordDO record = nursingRecordMapper.selectById(id);
+        if (record == null) {
+            throw exception(NURSING_RECORD_NOT_EXISTS);
+        }
+        NursingRecordRespVO respVO = BeanUtils.toBean(record, NursingRecordRespVO.class);
+        // 冗余的楼层房号床号为空时,按长者当前床位实时兜底
+        if (!StringUtils.hasText(respVO.getBedInfo())) {
+            respVO.setBedInfo(buildService.getFullBedName(respVO.getElderId()));
+        }
+        TenantDO tenantDO = tenantMapper.selectById(respVO.getTenantId());
+        respVO.setTenantName(tenantDO == null ? null : tenantDO.getName());
+        return respVO;
+    }
+
+    @Override
+    public PageResult<NursingRecordRespVO> getPage(NursingRecordPageReqVO pageReqVO) {
+        PageResult<NursingRecordRespVO> pageResult = nursingRecordMapper.selectPageWithBedInfo(pageReqVO);
+        for (NursingRecordRespVO respVO : pageResult.getList()) {
+            if (!StringUtils.hasText(respVO.getBedInfo())) {
+                respVO.setBedInfo(respVO.getBedName());
+            }
+        }
+        return pageResult;
+    }
+
+    private void validateExists(Long id) {
+        if (id == null || nursingRecordMapper.selectById(id) == null) {
+            throw exception(NURSING_RECORD_NOT_EXISTS);
+        }
+    }
+
+    /**
+     * 校验舒张压必须小于收缩压
+     */
+    private void validateBloodPressure(NursingRecordSaveReqVO reqVO) {
+        if (Objects.nonNull(reqVO.getSystolicPressure()) && Objects.nonNull(reqVO.getDiastolicPressure())
+                && reqVO.getDiastolicPressure() >= reqVO.getSystolicPressure()) {
+            throw exception(NURSING_RECORD_BLOOD_PRESSURE_INVALID);
+        }
+    }
+}

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

@@ -282,6 +282,25 @@ public class NurseChangeRecordServiceImpl implements ElderlyChangeRecordService
         return detail;
     }
 
+    @Override
+    @TenantIgnore
+    public FeeItemChangeDetailRespVO getFeeItemDetail(Long id, Integer status, Boolean isDetail) {
+        Integer currentFlag = 0;
+        if (null != status && status == 4) {
+            Integer count = bedChangeRecordMapper.checkFeeItemBpmBusinessFormById(id);
+            if (null == count || count == 0) {
+                currentFlag = 1;
+            }
+        }
+        FeeItemChangeDetailRespVO detail = isDetail
+                ? bedChangeRecordMapper.getFeeItemDetail(id, currentFlag)
+                : bedChangeRecordMapper.createFeeItemChangeGetDetail(id, currentFlag);
+        if (null != detail) {
+            detail.setBedName(buildService.getFullBedName(detail.getElderlyId()));
+        }
+        return detail;
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void swapBed(BedSwapReqVO reqVO) {

+ 93 - 0
yudao-module-system/yudao-module-system-biz/src/main/resources/mapper/ElderlyChangeRecordMapper.xml

@@ -754,6 +754,99 @@
         AND ee.created_time = (SELECT MAX(ee2.created_time) FROM elderly_expense ee2 WHERE ee2.elder_id = ei.id)
     </select>
 
+    <select id="getFeeItemDetail" resultType="cn.iocoder.yudao.module.system.controller.admin.biz.vo.FeeItemChangeDetailRespVO">
+        SELECT
+        ecr.id,
+        eei.item_name AS originalItemName,
+        eei.is_discount AS originalIsDiscount,
+        soc.price AS originalAmount,
+        soc.charge_name,
+        ei.elder_name,
+        ei.id_card,
+        ei.bed_id,
+        ecr.original_id,
+        ecr.original_name,
+        ecr.expect_id,
+        ecr.expect_name,
+        ecr.change_date,
+        ecr.is_private_room,
+        ecr.is_discount,
+        ecr.reason,
+        ecr.elderly_id,
+        ecr.change_files,
+        ecr.overhead_charge_id,
+        ecr.category_name,
+        ecr.amount,
+        ecr.actual_amount,
+        ecr.discount_amount,
+        ecr.discount,
+        ecr.create_time,
+        bbf.process_instance_id,
+        bbf.status,
+        bbf.start_tenant_id
+        FROM elderly_change_record ecr
+        INNER JOIN bpm_business_form bbf ON bbf.business_id = ecr.id AND bbf.type = 11
+        INNER JOIN elderly_info ei ON ecr.elderly_id = ei.id
+        INNER JOIN elderly_expense ee ON ei.id = ee.elder_id
+        INNER JOIN elderly_expense_item eei ON eei.expense_id = ee.id
+        INNER JOIN sys_charge_category scc ON scc.id = eei.item_category_id AND scc.identification NOT IN (1,2,3)
+        INNER JOIN sys_overhead_charge soc ON soc.id = ecr.overhead_charge_id
+        WHERE ecr.current_flag = #{currentFlag} AND ecr.id = #{id}
+        AND eei.item_id = ecr.overhead_charge_id
+        AND ee.created_time = (SELECT MAX(ee2.created_time) FROM elderly_expense ee2 WHERE ee2.elder_id = ei.id)
+    </select>
+
+    <select id="createFeeItemChangeGetDetail" resultType="cn.iocoder.yudao.module.system.controller.admin.biz.vo.FeeItemChangeDetailRespVO">
+        SELECT
+        ecr.id,
+        eei.item_name AS originalItemName,
+        eei.is_discount AS originalIsDiscount,
+        eei.actual_amount AS originalAmount,
+        soc.charge_name,
+        ei.elder_name,
+        ei.id_card,
+        ei.bed_id,
+        ecr.original_id,
+        ecr.original_name,
+        ecr.expect_id,
+        ecr.expect_name,
+        ecr.change_date,
+        ecr.is_private_room,
+        ecr.is_discount,
+        ecr.reason,
+        ecr.elderly_id,
+        ecr.change_files,
+        ecr.overhead_charge_id,
+        ecr.category_name,
+        ecr.amount,
+        ecr.actual_amount,
+        ecr.discount_amount,
+        ecr.discount,
+        ecr.create_time,
+        bbf.process_instance_id,
+        bbf.status,
+        bbf.start_tenant_id
+        FROM elderly_change_record ecr
+        INNER JOIN bpm_business_form bbf ON bbf.business_id = ecr.id AND bbf.type = 11
+        INNER JOIN elderly_info ei ON ecr.elderly_id = ei.id
+        INNER JOIN elderly_expense ee ON ei.id = ee.elder_id
+        INNER JOIN elderly_expense_item eei ON eei.expense_id = ee.id
+        INNER JOIN sys_charge_category scc ON scc.id = eei.item_category_id AND scc.identification NOT IN (1,2,3)
+        INNER JOIN sys_overhead_charge soc ON soc.id = ecr.overhead_charge_id
+        WHERE ecr.current_flag = #{currentFlag} AND ecr.id = #{id}
+        AND eei.item_id = ecr.overhead_charge_id
+        AND ee.created_time = (SELECT MAX(ee2.created_time) FROM elderly_expense ee2 WHERE ee2.elder_id = ei.id)
+    </select>
+
+    <select id="checkFeeItemBpmBusinessFormById" resultType="java.lang.Integer">
+        SELECT count(1)
+        FROM elderly_change_record ecr
+                 INNER JOIN bpm_business_form bbf ON bbf.business_id = ecr.id AND bbf.type = 11
+        WHERE bbf.business_id = #{id}
+          AND bbf.type = 11
+          AND ecr.current_flag = 0
+    </select>
+
     <select id="selectDetail" resultType="cn.iocoder.yudao.module.system.controller.admin.biz.vo.TransferChangeDetailRespVO">
         SELECT
             ecr.id,

+ 37 - 0
yudao-module-system/yudao-module-system-biz/src/main/resources/mapper/ElderlyNursingRecordMapper.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.system.dal.mysql.biz.ElderlyNursingRecordMapper">
+
+    <select id="selectPageWithBedInfo"
+            resultType="cn.iocoder.yudao.module.system.controller.admin.biz.vo.nursingrecord.NursingRecordRespVO">
+        SELECT
+            t.*,
+            CONCAT_WS('-', ob.build_name, obf.floor_name, obr.room_name, obb.bed_name) AS bedName,
+            st.name AS tenantName
+        FROM elderly_nursing_record t
+        LEFT JOIN elderly_info ei ON ei.id = t.elder_id
+        LEFT JOIN org_build_bed obb ON obb.id = ei.bed_id AND obb.tenant_id = t.tenant_id
+        LEFT JOIN org_build_room obr ON obr.id = obb.room_id AND obr.tenant_id = obb.tenant_id
+        LEFT JOIN org_build_floor obf ON obf.id = obr.floor_id AND obf.tenant_id = obr.tenant_id
+        LEFT JOIN org_build ob ON ob.id = obf.build_id AND ob.tenant_id = obf.tenant_id
+        LEFT JOIN system_tenant st ON st.id = t.tenant_id
+        WHERE t.deleted = 0
+        <if test="reqVO.tenantIds != null and reqVO.tenantIds.length > 0">
+            AND t.tenant_id IN
+            <foreach item="id" collection="reqVO.tenantIds" open="(" separator="," close=")">
+                #{id}
+            </foreach>
+        </if>
+        <if test="reqVO.elderName != null and reqVO.elderName != ''">
+            AND t.elder_name LIKE CONCAT('%', #{reqVO.elderName}, '%')
+        </if>
+        <if test="reqVO.recordDate != null and reqVO.recordDate.length >= 1">
+            AND t.record_date &gt;= #{reqVO.recordDate[0]}
+        </if>
+        <if test="reqVO.recordDate != null and reqVO.recordDate.length == 2">
+            AND t.record_date &lt;= #{reqVO.recordDate[1]}
+        </if>
+        ORDER BY t.record_date DESC, t.record_time DESC, t.id DESC
+    </select>
+
+</mapper>