Ver Fonte

新增
1、新增是否自动执行长期医嘱记录。
修改
1、重构长期医嘱和执行医嘱模块。

liangwenxuan há 7 horas atrás
pai
commit
d32eefdecd
18 ficheiros alterados com 449 adições e 23 exclusões
  1. 3 0
      sql/mysql/alter_elderly_long_doctor_order_entry_add_plan_exec_time.sql
  2. 1 0
      sql/mysql/elderly_doctor_order.sql
  3. 25 0
      sql/mysql/elderly_doctor_order_exec_auto_complete.sql
  4. 7 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/NoticeConstants.java
  5. 2 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/doctororder/vo/LongDoctorOrderRespVO.java
  6. 2 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/doctororder/vo/LongDoctorOrderSaveReqVO.java
  7. 17 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sys/BusinessParamsConfigController.java
  8. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/biz/DoctorOrderExecutionDO.java
  9. 2 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/biz/LongDoctorOrderEntryDO.java
  10. 11 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sys/BusinessParamsConfigMapper.java
  11. 67 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/job/LongDoctorOrderExecJob.java
  12. 32 5
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/appointment/AppAppointmentQuotaServiceImpl.java
  13. 31 17
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyInfoServiceImpl.java
  14. 21 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/doctororder/DoctorOrderExecutionService.java
  15. 144 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/doctororder/DoctorOrderExecutionServiceImpl.java
  16. 12 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/doctororder/LongDoctorOrderServiceImpl.java
  17. 25 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sys/BusinessParamsConfigService.java
  18. 46 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sys/BusinessParamsConfigServiceImpl.java

+ 3 - 0
sql/mysql/alter_elderly_long_doctor_order_entry_add_plan_exec_time.sql

@@ -0,0 +1,3 @@
+-- 长期医嘱条目:新增「计划执行时间」字段(HH:mm),用于定时任务按「日期 + 计划执行时间」生成护士执行记录
+ALTER TABLE `elderly_long_doctor_order_entry`
+    ADD COLUMN `plan_exec_time` VARCHAR(20) DEFAULT '' COMMENT '计划执行时间 HH:mm' AFTER `route`;

+ 1 - 0
sql/mysql/elderly_doctor_order.sql

@@ -28,6 +28,7 @@ CREATE TABLE elderly_long_doctor_order_entry (
   frequency         VARCHAR(20)  DEFAULT '' COMMENT '给药频次缩写',
   med_time          VARCHAR(20)  DEFAULT '' COMMENT '给药时间缩写',
   route             VARCHAR(20)  DEFAULT '' COMMENT '给药途径缩写',
+  plan_exec_time    VARCHAR(20)  DEFAULT '' COMMENT '计划执行时间 HH:mm,定时任务按「日期 + 计划执行时间」生成执行记录',
   doctor_sign       VARCHAR(64)  DEFAULT '' COMMENT '医生签名',
   nurse_sign        VARCHAR(64)  DEFAULT '' COMMENT '护士签名',
   stop_time         DATETIME     DEFAULT NULL COMMENT '停止时间',

+ 25 - 0
sql/mysql/elderly_doctor_order_exec_auto_complete.sql

@@ -0,0 +1,25 @@
+-- ============================================================
+-- 长期医嘱执行记录:定时任务注册 + 机构「执行记录自动完成」配置初始化
+-- ============================================================
+
+-- 1. 为每个机构初始化「执行记录自动完成」配置(默认关闭)
+INSERT INTO `system_business_params_config`
+(`title`, `config_name`, `config_code`, `value`, `description`, `tenant_id`, `enable_status`)
+SELECT '医嘱执行配置', '执行记录自动完成设置', 'DOCTOR_ORDER_EXEC_AUTO_COMPLETE', '0',
+       '开启后,长期医嘱定时任务生成的护士执行记录自动置为已执行', t.`id`, 0
+FROM `system_tenant` t
+WHERE NOT EXISTS (
+    SELECT 1 FROM (SELECT `tenant_id`, `config_code` FROM `system_business_params_config`) c
+    WHERE c.`tenant_id` = t.`id` AND c.`config_code` = 'DOCTOR_ORDER_EXEC_AUTO_COMPLETE'
+);
+
+-- 2. 注册定时任务:每天 00:10 为未停止的长期医嘱条目生成当天执行记录
+INSERT INTO `infra_job`
+(`name`, `status`, `handler_name`, `handler_param`, `cron_expression`, `retry_count`, `retry_interval`, `monitor_timeout`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+SELECT '长期医嘱执行记录生成 Job', 2, 'longDoctorOrderExecJob', '', '0 10 0 * * ?', 3, 0, 0,
+       '1', NOW(), '1', NOW(), b'0'
+FROM DUAL
+WHERE NOT EXISTS (
+    SELECT 1 FROM (SELECT `handler_name` FROM `infra_job` WHERE `deleted` = b'0') j
+    WHERE j.`handler_name` = 'longDoctorOrderExecJob'
+);

+ 7 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/NoticeConstants.java

@@ -34,6 +34,13 @@ public interface NoticeConstants {
 
     String BUSINESS_CONFIG_TITLE = "业务流转通知配置";
 
+    /**
+     * 医嘱执行配置(按机构)
+     */
+    String DOCTOR_ORDER_CONFIG_TITLE = "医嘱执行配置";
+    String DOCTOR_ORDER_EXEC_AUTO_COMPLETE_CODE = "DOCTOR_ORDER_EXEC_AUTO_COMPLETE";
+    String DOCTOR_ORDER_EXEC_AUTO_COMPLETE_NAME = "执行记录自动完成设置";
+
 
 
 

+ 2 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/doctororder/vo/LongDoctorOrderRespVO.java

@@ -72,6 +72,8 @@ public class LongDoctorOrderRespVO {
         private String medTime;
         @Schema(description = "给药途径缩写")
         private String route;
+        @Schema(description = "计划执行时间,HH:mm,用于按天生成执行记录")
+        private String planExecTime;
         @Schema(description = "医生签名")
         private String doctorSign;
         @Schema(description = "护士签名")

+ 2 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/doctororder/vo/LongDoctorOrderSaveReqVO.java

@@ -45,6 +45,8 @@ public class LongDoctorOrderSaveReqVO {
         private String medTime;
         @Schema(description = "给药途径缩写")
         private String route;
+        @Schema(description = "计划执行时间,HH:mm,用于按天生成执行记录")
+        private String planExecTime;
         @Schema(description = "医生签名")
         private String doctorSign;
         @Schema(description = "护士签名")

+ 17 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sys/BusinessParamsConfigController.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.system.controller.admin.sys;
 
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.module.system.controller.admin.sys.vo.BusinessConfigUpdateReqVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.sys.BusinessParamsConfigDO;
 import cn.iocoder.yudao.module.system.service.sys.BusinessParamsConfigService;
@@ -47,6 +48,22 @@ public class BusinessParamsConfigController {
         return success(true);
     }
 
+    @GetMapping("/getDoctorOrderExecAutoComplete")
+    @Operation(summary = "查询某机构是否开启「执行记录自动完成」")
+    public CommonResult<Boolean> getDoctorOrderExecAutoComplete(@RequestParam(value = "tenantId", required = false) Long tenantId) {
+        Long targetTenantId = tenantId != null ? tenantId : TenantContextHolder.getTenantId();
+        return success(businessParamsConfigService.isDoctorOrderExecAutoComplete(targetTenantId));
+    }
+
+    @PutMapping("/updateDoctorOrderExecAutoComplete")
+    @Operation(summary = "设置某机构是否开启「执行记录自动完成」(开启后定时任务生成的执行记录自动置为已执行)")
+    public CommonResult<Boolean> updateDoctorOrderExecAutoComplete(@RequestParam("enable") Boolean enable,
+                                                                   @RequestParam(value = "tenantId", required = false) Long tenantId) {
+        Long targetTenantId = tenantId != null ? tenantId : TenantContextHolder.getTenantId();
+        businessParamsConfigService.updateDoctorOrderExecAutoComplete(targetTenantId, enable);
+        return success(true);
+    }
+
 //
 //    @DeleteMapping("/delete")
 //    @Operation(summary = "删除系统业务参数配置")

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/biz/DoctorOrderExecutionDO.java

@@ -43,7 +43,7 @@ public class DoctorOrderExecutionDO extends BaseNoDeleteDO {
     private String medTime;
     /** 给药途径缩写 */
     private String route;
-    /** 计划执行时间,前端生成 */
+    /** 计划执行时间*/
     private LocalDateTime planTime;
     /** 实际执行时间 */
     private LocalDateTime execTime;

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

@@ -35,6 +35,8 @@ public class LongDoctorOrderEntryDO extends BaseNoDeleteDO {
     private String medTime;
     /** 给药途径缩写 */
     private String route;
+    /** 计划执行时间,HH:mm 格式字符串,定时任务按「日期 + 计划执行时间」拼接生成执行记录 */
+    private String planExecTime;
     /** 医生签名 */
     private String doctorSign;
     /** 护士签名 */

+ 11 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sys/BusinessParamsConfigMapper.java

@@ -26,6 +26,17 @@ public interface BusinessParamsConfigMapper extends BaseMapperX<BusinessParamsCo
                 .in(BusinessParamsConfigDO::getConfigName, configNameList));
     };
 
+    /**
+     * 按机构 + 配置编码查询(需 @TenantIgnore,避免租户插件重复拼接 tenant_id)
+     */
+    @TenantIgnore
+    default BusinessParamsConfigDO selectByTenantAndCode(Long tenantId, String configCode) {
+        return selectOne(new LambdaQueryWrapperX<BusinessParamsConfigDO>()
+                .eq(BusinessParamsConfigDO::getTenantId, tenantId)
+                .eq(BusinessParamsConfigDO::getConfigCode, configCode)
+                .last("LIMIT 1"));
+    }
+
 //    default PageResult<BusinessParamsConfigDO> selectPage(BusinessParamsConfigPageReqVO reqVO) {
 //        return selectPage(reqVO, new LambdaQueryWrapperX<BusinessParamsConfigDO>()
 //                .eqIfPresent(BusinessParamsConfigDO::getTitle, reqVO.getTitle())

+ 67 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/job/LongDoctorOrderExecJob.java

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.system.job;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
+import cn.iocoder.yudao.module.system.service.doctororder.DoctorOrderExecutionService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.time.LocalDate;
+
+/**
+ * 长期医嘱执行记录定时生成 Job
+ * 按「日期 + 条目计划执行时间(HH:mm)」为每个未停止的长期医嘱条目生成当天的护士执行记录。
+ * 机构开启「执行记录自动完成」时,生成的记录直接置为已执行。
+ *
+ * @author 系统
+ */
+@Component
+@Slf4j
+public class LongDoctorOrderExecJob implements JobHandler {
+
+    /**
+     * 任务参数中的日期偏移天数 key,如 {"offsetDays":1} 表示生成明天
+     */
+    private static final String PARAM_OFFSET_DAYS = "offsetDays";
+
+    @Resource
+    private DoctorOrderExecutionService doctorOrderExecutionService;
+
+    @Override
+    @TenantJob
+    public String execute(String param) throws Exception {
+        Long tenantId = TenantContextHolder.getTenantId();
+        LocalDate date = LocalDate.now().plusDays(parseOffsetDays(param));
+        int count = doctorOrderExecutionService.generateLongOrderExecutions(date);
+        log.info("[execute][机构({}) 长期医嘱执行记录生成完成,日期({}),新增({})条]", tenantId, date, count);
+        return String.format("机构%s 日期%s 新增%s条", tenantId, date, count);
+    }
+
+    /**
+     * 解析任务参数:支持纯数字(偏移天数)或 JSON({"offsetDays":1}),为空时默认当天
+     */
+    private int parseOffsetDays(String param) {
+        if (StrUtil.isBlank(param)) {
+            return 0;
+        }
+        String text = param.trim();
+        try {
+            return Integer.parseInt(text);
+        } catch (NumberFormatException ignored) {
+            // 继续按 JSON 解析
+        }
+        try {
+            JSONObject json = JSONUtil.parseObj(text);
+            return json.getInt(PARAM_OFFSET_DAYS, 0);
+        } catch (Exception ex) {
+            log.warn("[parseOffsetDays][任务参数({}) 解析失败,按当天处理]", param);
+            return 0;
+        }
+    }
+
+}

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

@@ -426,15 +426,42 @@ public class AppAppointmentQuotaServiceImpl implements AppAppointmentQuotaServic
     }
 
     private boolean matchCustomDates(String customDates, LocalDate date) {
-        if (customDates == null || customDates.trim().isEmpty()) {
-            return false;
-        }
-        List<String> dates = JsonUtils.parseArray(customDates, String.class);
-        if (dates == null) {
+        List<String> dates = parseDateList(customDates);
+        if (dates == null || dates.isEmpty()) {
             return false;
         }
         String ds = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
         return dates.contains(ds);
     }
 
+    /**
+     * 解析自定义日期列表,兼容多种存储格式:
+     * 1. JSON 数组:["2026-10-15","2026-10-17"]
+     * 2. 逗号分隔:2026-10-15,2026-10-17
+     */
+    private List<String> parseDateList(String value) {
+        if (value == null || value.trim().isEmpty()) {
+            return null;
+        }
+        String text = value.trim();
+        if (text.startsWith("[")) {
+            try {
+                List<String> list = JsonUtils.parseArray(text, String.class);
+                if (list != null) {
+                    return list;
+                }
+            } catch (Exception e) {
+                // JSON 解析失败,降级按逗号分隔解析
+            }
+        }
+        List<String> result = new ArrayList<>();
+        for (String s : text.replace("[", "").replace("]", "").replace("\"", "").split(",")) {
+            String item = s.trim();
+            if (!item.isEmpty()) {
+                result.add(item);
+            }
+        }
+        return result;
+    }
+
 }

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

@@ -1753,34 +1753,48 @@ public class ElderlyInfoServiceImpl implements ElderlyInfoService {
                 .between(ElderlyRetreatRecordDO::getRetreatDate, today, today))).orElse(0L);
 
         List<ElderlyInfoDO> inHospitalElders = elderlyInfoMapper.selectList(new LambdaQueryWrapperX<ElderlyInfoDO>()
-                .select(ElderlyInfoDO::getNurseLevelId)
+                .select(ElderlyInfoDO::getNurseLevelId, ElderlyInfoDO::getNurseLevelName)
                 .eq(ElderlyInfoDO::getInStatus, 1)
                 .eq(ElderlyInfoDO::getDeleted, 0)
                 .eq(ElderlyInfoDO::getOrgType, 1)
                 .in(ElderlyInfoDO::getTenantId, tenantIdList));
 
-        Map<Long, Long> nurseLevelCountMap = inHospitalElders.stream()
+        Set<Long> levelIdSet = inHospitalElders.stream()
                 .map(ElderlyInfoDO::getNurseLevelId)
                 .filter(Objects::nonNull)
-                .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
+                .collect(Collectors.toSet());
 
-        long noFeeCount = inHospitalElders.stream().filter(e -> e.getNurseLevelId() == null).count();
-
-        Map<Long, String> levelNameMap = nurseLevelCountMap.isEmpty()
+        // 护理等级 id -> 护理等级名称(以护理等级表为准,档案冗余名称兜底)
+        Map<Long, String> levelNameMap = levelIdSet.isEmpty()
                 ? new HashMap<>()
-                : nurseLevelMapper.selectBatchIds(nurseLevelCountMap.keySet()).stream()
+                : nurseLevelMapper.selectBatchIds(levelIdSet).stream()
                 .collect(Collectors.toMap(NurseLevelDO::getId, NurseLevelDO::getNurseLevelName, (a, b) -> a));
 
-        List<InstitutionDashboardRespVO.StatisticItemVO> nursingSituation = new ArrayList<>();
-        nurseLevelCountMap.entrySet().stream()
-                .sorted(Map.Entry.comparingByKey())
-                .forEach(entry -> {
-                    String levelName = levelNameMap.get(entry.getKey());
-                    if (StringUtils.isBlank(levelName)) {
-                        levelName = "未知等级";
-                    }
-                    nursingSituation.add(buildStatisticItem(levelName, entry.getValue()));
-                });
+        // 按护理等级名称统计(同名等级合并统计),并记录同名等级中的最小 id 用于排序
+        Map<String, Long> nurseLevelCountMap = new LinkedHashMap<>();
+        Map<String, Long> nurseLevelOrderMap = new HashMap<>();
+        long noFeeCount = 0L;
+        for (ElderlyInfoDO elder : inHospitalElders) {
+            Long levelId = elder.getNurseLevelId();
+            if (levelId == null) {
+                noFeeCount++;
+                continue;
+            }
+            String levelName = levelNameMap.get(levelId);
+            if (StringUtils.isBlank(levelName)) {
+                levelName = elder.getNurseLevelName();
+            }
+            if (StringUtils.isBlank(levelName)) {
+                levelName = "未知等级";
+            }
+            nurseLevelCountMap.merge(levelName, 1L, Long::sum);
+            nurseLevelOrderMap.merge(levelName, levelId, Math::min);
+        }
+
+        List<InstitutionDashboardRespVO.StatisticItemVO> nursingSituation = nurseLevelCountMap.entrySet().stream()
+                .sorted(Comparator.comparing(e -> nurseLevelOrderMap.get(e.getKey())))
+                .map(entry -> buildStatisticItem(entry.getKey(), entry.getValue()))
+                .collect(Collectors.toList());
         if (noFeeCount > 0) {
             nursingSituation.add(buildStatisticItem("无费用", noFeeCount));
         }

+ 21 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/doctororder/DoctorOrderExecutionService.java

@@ -4,6 +4,10 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.system.controller.admin.doctororder.vo.DoctorOrderExecutionPageReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.doctororder.vo.DoctorOrderExecutionRespVO;
 import cn.iocoder.yudao.module.system.controller.admin.doctororder.vo.DoctorOrderExecutionSaveReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.LongDoctorOrderEntryDO;
+
+import java.time.LocalDate;
+import java.util.List;
 
 /**
  * 护士执行记录 Service
@@ -25,4 +29,21 @@ public interface DoctorOrderExecutionService {
      */
     void skipDoctorOrderExecution(Long id);
 
+    /**
+     * 为新增的长期医嘱条目补建常规执行记录(取当天日期 + 条目计划执行时间)
+     *
+     * @param entries 新增的长期医嘱条目
+     * @return 新增记录数
+     */
+    int generateExecutionsForNewEntries(List<LongDoctorOrderEntryDO> entries);
+
+    /**
+     * 定时任务:为当前机构所有未停止的长期医嘱条目生成指定日期的常规执行记录
+     * 计划执行时间 = 日期 + 条目的计划执行时间(HH:mm)
+     *
+     * @param date 计划执行日期,为空时取当天
+     * @return 新增记录数
+     */
+    int generateLongOrderExecutions(LocalDate date);
+
 }

+ 144 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/doctororder/DoctorOrderExecutionServiceImpl.java

@@ -1,5 +1,7 @@
 package cn.iocoder.yudao.module.system.service.doctororder;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
@@ -8,24 +10,37 @@ import cn.iocoder.yudao.module.system.controller.admin.doctororder.vo.DoctorOrde
 import cn.iocoder.yudao.module.system.controller.admin.doctororder.vo.DoctorOrderExecutionSaveReqVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.biz.DoctorOrderExecutionDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.biz.ElderlyInfoDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.LongDoctorOrderDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.biz.LongDoctorOrderEntryDO;
 import cn.iocoder.yudao.module.system.dal.mysql.biz.DoctorOrderExecutionMapper;
 import cn.iocoder.yudao.module.system.dal.mysql.biz.ElderlyInfoMapper;
 import cn.iocoder.yudao.module.system.dal.mysql.biz.LongDoctorOrderEntryMapper;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.LongDoctorOrderMapper;
 import cn.iocoder.yudao.module.system.enums.ErrorCodeConstants;
 import cn.iocoder.yudao.module.system.enums.biz.DoctorOrderExecTypeEnum;
 import cn.iocoder.yudao.module.system.enums.biz.DoctorOrderExecutionStatusEnum;
 import cn.iocoder.yudao.module.system.enums.biz.DoctorOrderTypeEnum;
+import cn.iocoder.yudao.module.system.enums.biz.LongOrderEntryStatusEnum;
+import cn.iocoder.yudao.module.system.service.sys.BusinessParamsConfigService;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.time.Period;
 import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 /**
@@ -35,14 +50,28 @@ import java.util.stream.Collectors;
  */
 @Service
 @Validated
+@Slf4j
 public class DoctorOrderExecutionServiceImpl implements DoctorOrderExecutionService {
 
+    /**
+     * 计划执行时间支持的格式,兼容 HH:mm / H:mm / HH:mm:ss / H:mm:ss
+     */
+    private static final List<DateTimeFormatter> PLAN_EXEC_TIME_FORMATTERS = Arrays.asList(
+            DateTimeFormatter.ofPattern("HH:mm"),
+            DateTimeFormatter.ofPattern("H:mm"),
+            DateTimeFormatter.ofPattern("HH:mm:ss"),
+            DateTimeFormatter.ofPattern("H:mm:ss"));
+
     @Resource
     private DoctorOrderExecutionMapper doctorOrderExecutionMapper;
     @Resource
     private ElderlyInfoMapper elderlyInfoMapper;
     @Resource
     private LongDoctorOrderEntryMapper longDoctorOrderEntryMapper;
+    @Resource
+    private LongDoctorOrderMapper longDoctorOrderMapper;
+    @Resource
+    private BusinessParamsConfigService businessParamsConfigService;
 
     @Override
     public PageResult<DoctorOrderExecutionRespVO> getDoctorOrderExecutionPage(DoctorOrderExecutionPageReqVO reqVO) {
@@ -106,6 +135,121 @@ public class DoctorOrderExecutionServiceImpl implements DoctorOrderExecutionServ
         doctorOrderExecutionMapper.updateById(exist);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int generateExecutionsForNewEntries(List<LongDoctorOrderEntryDO> entries) {
+        if (CollUtil.isEmpty(entries)) {
+            return 0;
+        }
+        // 同一个主单的条目复用一份主单信息,避免重复查询
+        Map<Long, LongDoctorOrderDO> orderCache = new HashMap<>();
+        int count = 0;
+        for (LongDoctorOrderEntryDO entry : entries) {
+            if (entry == null || entry.getLongDoctorOrderId() == null) {
+                continue;
+            }
+            LongDoctorOrderDO orderDO = orderCache.computeIfAbsent(entry.getLongDoctorOrderId(),
+                    id -> longDoctorOrderMapper.selectById(id));
+            count += doGenerate(Collections.singletonList(entry), orderDO, LocalDate.now());
+        }
+        return count;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int generateLongOrderExecutions(LocalDate date) {
+        LocalDate targetDate = date == null ? LocalDate.now() : date;
+        List<LongDoctorOrderDO> orders = longDoctorOrderMapper.selectList();
+        int count = 0;
+        for (LongDoctorOrderDO orderDO : orders) {
+            List<LongDoctorOrderEntryDO> entries = longDoctorOrderEntryMapper.selectListByOrderId(orderDO.getId());
+            count += doGenerate(entries, orderDO, targetDate);
+        }
+        log.info("[generateLongOrderExecutions][机构({}) 日期({}) 生成长期医嘱执行记录({})条]",
+                TenantContextHolder.getTenantId(), targetDate, count);
+        return count;
+    }
+
+    /**
+     * 按「日期 + 条目计划执行时间」生成常规执行记录,已存在(唯一键)则跳过
+     */
+    private int doGenerate(List<LongDoctorOrderEntryDO> entries, LongDoctorOrderDO orderDO, LocalDate date) {
+        if (CollUtil.isEmpty(entries) || orderDO == null) {
+            return 0;
+        }
+        Long tenantId = orderDO.getTenantId() != null ? orderDO.getTenantId() : TenantContextHolder.getTenantId();
+        // 机构配置了「执行记录自动完成」,则生成的记录直接置为已执行
+        boolean autoComplete = businessParamsConfigService.isDoctorOrderExecAutoComplete(tenantId);
+        int count = 0;
+        for (LongDoctorOrderEntryDO entry : entries) {
+            if (entry == null || LongOrderEntryStatusEnum.STOPPED.getStatus().equals(entry.getStatus())) {
+                // 已停止的条目不再生成执行记录(停止流程由停止确认任务闭环)
+                continue;
+            }
+            LocalTime planTimeOfDay = parsePlanExecTime(entry.getPlanExecTime());
+            if (planTimeOfDay == null) {
+                continue;
+            }
+            LocalDateTime planTime = date.atTime(planTimeOfDay);
+            Integer execType = DoctorOrderExecTypeEnum.NORMAL.getType();
+            // 是否必须?
+            if (doctorOrderExecutionMapper.selectByUniqueKey(
+                    DoctorOrderTypeEnum.LONG.getType(), entry.getId(), planTime, execType) != null) {
+                continue;
+            }
+            DoctorOrderExecutionDO execDO = new DoctorOrderExecutionDO();
+            fillNormalTask(execDO, entry, orderDO, planTime, tenantId);
+            if (autoComplete) {
+                execDO.setStatus(DoctorOrderExecutionStatusEnum.DONE.getStatus());
+                execDO.setExecTime(planTime);
+            } else {
+                execDO.setStatus(DoctorOrderExecutionStatusEnum.PENDING.getStatus());
+            }
+            doctorOrderExecutionMapper.insert(execDO);
+            count++;
+        }
+        return count;
+    }
+
+    /**
+     * 填充常规执行记录字段(长期医嘱条目 -> 护士执行记录)
+     */
+    private void fillNormalTask(DoctorOrderExecutionDO execDO, LongDoctorOrderEntryDO entry,
+                                LongDoctorOrderDO orderDO, LocalDateTime planTime, Long tenantId) {
+        execDO.setOrderType(DoctorOrderTypeEnum.LONG.getType());
+        execDO.setExecType(DoctorOrderExecTypeEnum.NORMAL.getType());
+        execDO.setOrderEntryId(entry.getId());
+        execDO.setElderId(orderDO.getElderId());
+        execDO.setElderName(orderDO.getElderName());
+        execDO.setBedName(orderDO.getBedName());
+        execDO.setOrderContent(entry.getOrderContent());
+        execDO.setFrequency(entry.getFrequency());
+        execDO.setMedTime(entry.getMedTime());
+        execDO.setRoute(entry.getRoute());
+        execDO.setPlanTime(planTime);
+        execDO.setDoctorSign(entry.getDoctorSign());
+        execDO.setTenantId(tenantId);
+    }
+
+    /**
+     * 解析计划执行时间(HH:mm)
+     */
+    private LocalTime parsePlanExecTime(String planExecTime) {
+        if (StrUtil.isBlank(planExecTime)) {
+            return null;
+        }
+        String text = planExecTime.trim();
+        for (DateTimeFormatter formatter : PLAN_EXEC_TIME_FORMATTERS) {
+            try {
+                return LocalTime.parse(text, formatter);
+            } catch (DateTimeParseException ignored) {
+                // 尝试下一种格式
+            }
+        }
+        log.warn("[parsePlanExecTime][计划执行时间({}) 格式非法,跳过生成执行记录]", planExecTime);
+        return null;
+    }
+
     /**
      * 定位待执行/确认的记录:停止确认优先按 id、其次按条目查待确认任务;常规执行沿用唯一键 upsert
      */

+ 12 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/doctororder/LongDoctorOrderServiceImpl.java

@@ -21,6 +21,7 @@ import cn.iocoder.yudao.module.system.enums.biz.DoctorOrderTypeEnum;
 import cn.iocoder.yudao.module.system.enums.biz.LongOrderEntryStatusEnum;
 import cn.iocoder.yudao.module.system.enums.ErrorCodeConstants;
 import cn.iocoder.yudao.module.system.service.biz.BuildService;
+import cn.iocoder.yudao.module.system.service.doctororder.DoctorOrderExecutionService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.dao.DuplicateKeyException;
 import org.springframework.stereotype.Service;
@@ -55,6 +56,8 @@ public class LongDoctorOrderServiceImpl implements LongDoctorOrderService {
     private BuildService buildService;
     @Resource
     private DoctorOrderExecutionMapper doctorOrderExecutionMapper;
+    @Resource
+    private DoctorOrderExecutionService doctorOrderExecutionService;
 
     @Override
     public PageResult<LongDoctorOrderRespVO> getLongDoctorOrderPage(LongDoctorOrderPageReqVO reqVO) {
@@ -85,6 +88,8 @@ public class LongDoctorOrderServiceImpl implements LongDoctorOrderService {
         fillOrderBase(orderDO, elder);
         longDoctorOrderMapper.insert(orderDO);
 
+        // 新增完成后,按「当天日期 + 条目计划执行时间」同步生成护士执行记录
+        List<LongDoctorOrderEntryDO> newEntries = new ArrayList<>();
         if (CollUtil.isNotEmpty(reqVO.getEntries())) {
             for (LongDoctorOrderSaveReqVO.Entry entry : reqVO.getEntries()) {
                 LongDoctorOrderEntryDO entryDO = new LongDoctorOrderEntryDO();
@@ -94,8 +99,10 @@ public class LongDoctorOrderServiceImpl implements LongDoctorOrderService {
                 }
                 entryDO.setLongDoctorOrderId(orderDO.getId());
                 longDoctorOrderEntryMapper.insert(entryDO);
+                newEntries.add(entryDO);
             }
         }
+        doctorOrderExecutionService.generateExecutionsForNewEntries(newEntries);
         return orderDO.getId();
     }
 
@@ -117,6 +124,8 @@ public class LongDoctorOrderServiceImpl implements LongDoctorOrderService {
         List<LongDoctorOrderEntryDO> existEntries = longDoctorOrderEntryMapper.selectListByOrderId(orderDO.getId());
         Set<Long> existIds = existEntries.stream().map(LongDoctorOrderEntryDO::getId).collect(Collectors.toSet());
         Set<Long> reqIds = new HashSet<>();
+        // 只有「新增明细」才补建执行记录;停用明细走 /entry/updateStatus,由停止确认任务闭环,此处不处理
+        List<LongDoctorOrderEntryDO> newEntries = new ArrayList<>();
         if (CollUtil.isNotEmpty(reqVO.getEntries())) {
             for (LongDoctorOrderSaveReqVO.Entry entry : reqVO.getEntries()) {
                 if (entry.getId() != null) {
@@ -135,6 +144,7 @@ public class LongDoctorOrderServiceImpl implements LongDoctorOrderService {
                     }
                     entryDO.setLongDoctorOrderId(orderDO.getId());
                     longDoctorOrderEntryMapper.insert(entryDO);
+                    newEntries.add(entryDO);
                 }
             }
         }
@@ -144,6 +154,7 @@ public class LongDoctorOrderServiceImpl implements LongDoctorOrderService {
                 longDoctorOrderEntryMapper.deleteById(existId);
             }
         }
+        doctorOrderExecutionService.generateExecutionsForNewEntries(newEntries);
     }
 
     @Override
@@ -295,6 +306,7 @@ public class LongDoctorOrderServiceImpl implements LongDoctorOrderService {
         entryDO.setFrequency(entry.getFrequency());
         entryDO.setMedTime(entry.getMedTime());
         entryDO.setRoute(entry.getRoute());
+        entryDO.setPlanExecTime(entry.getPlanExecTime());
         entryDO.setDoctorSign(entry.getDoctorSign());
         entryDO.setNurseSign(entry.getNurseSign());
         entryDO.setStopTime(entry.getStopTime());

+ 25 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sys/BusinessParamsConfigService.java

@@ -21,4 +21,29 @@ public interface BusinessParamsConfigService {
     List<BusinessParamsConfigDO> getMonthlySettlementConfigByStatus(Integer value);
 
     void batchUpdate(BusinessConfigUpdateReqVO updateReqVO);
+
+    /**
+     * 判断某机构是否开启了「执行记录自动完成」
+     * 开启后,长期医嘱定时任务自动生成的护士执行记录直接置为已执行
+     *
+     * @param tenantId 机构编号
+     * @return true 表示自动完成
+     */
+    boolean isDoctorOrderExecAutoComplete(Long tenantId);
+
+    /**
+     * 查询某机构的「执行记录自动完成」配置,不存在则返回 null
+     *
+     * @param tenantId 机构编号
+     * @return 配置
+     */
+    BusinessParamsConfigDO getDoctorOrderExecAutoCompleteConfig(Long tenantId);
+
+    /**
+     * 设置某机构是否开启「执行记录自动完成」
+     *
+     * @param tenantId 机构编号
+     * @param enable true 开启
+     */
+    void updateDoctorOrderExecAutoComplete(Long tenantId, Boolean enable);
 }

+ 46 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sys/BusinessParamsConfigServiceImpl.java

@@ -49,6 +49,52 @@ public class BusinessParamsConfigServiceImpl implements BusinessParamsConfigServ
         return businessParamsConfigList;
     }
 
+    @Override
+    public BusinessParamsConfigDO getDoctorOrderExecAutoCompleteConfig(Long tenantId) {
+        if (tenantId == null) {
+            return null;
+        }
+        return businessParamsConfigMapper.selectByTenantAndCode(
+                tenantId, NoticeConstants.DOCTOR_ORDER_EXEC_AUTO_COMPLETE_CODE);
+    }
+
+    @Override
+    public boolean isDoctorOrderExecAutoComplete(Long tenantId) {
+        BusinessParamsConfigDO config = getDoctorOrderExecAutoCompleteConfig(tenantId);
+        if (config == null) {
+            return false;
+        }
+        if (Integer.valueOf(1).equals(config.getEnableStatus())) {
+            return true;
+        }
+        // 兼容只维护 value 的场景
+        return "1".equals(config.getValue()) || "true".equalsIgnoreCase(config.getValue());
+    }
+
+    @Override
+    public void updateDoctorOrderExecAutoComplete(Long tenantId, Boolean enable) {
+        if (tenantId == null) {
+            return;
+        }
+        boolean enabled = Boolean.TRUE.equals(enable);
+        BusinessParamsConfigDO config = getDoctorOrderExecAutoCompleteConfig(tenantId);
+        if (config == null) {
+            config = new BusinessParamsConfigDO();
+            config.setTitle(NoticeConstants.DOCTOR_ORDER_CONFIG_TITLE);
+            config.setConfigName(NoticeConstants.DOCTOR_ORDER_EXEC_AUTO_COMPLETE_NAME);
+            config.setConfigCode(NoticeConstants.DOCTOR_ORDER_EXEC_AUTO_COMPLETE_CODE);
+            config.setDescription("开启后,长期医嘱定时任务生成的护士执行记录自动置为已执行");
+            config.setTenantId(tenantId);
+        }
+        config.setEnableStatus(enabled ? 1 : 0);
+        config.setValue(enabled ? "1" : "0");
+        if (config.getId() == null) {
+            businessParamsConfigMapper.insert(config);
+        } else {
+            businessParamsConfigMapper.updateById(config);
+        }
+    }
+
     @Override
     public void batchUpdate(BusinessConfigUpdateReqVO updateReqVO) {
         if (NoticeConstants.BUSINESS_CONFIG_TITLE.equals(updateReqVO.getTitle())) {