gaoluyang
昨天 2eb261814a2407b3481dfe7b995c58d413a18cff
feat(quality): 质量监督/质量监控流程对齐程序文件并完善导入

- 质量监督记录增加监督方式、被监督人确认意见,监督内容文字对齐受控表 LMSY-CX29-JL02
- 监督日期兼容 2026/3/15、2026-03-15 等历史格式,导入时统一为“年.月”
- 导出记录单支持被监督人确认意见动态渲染
- 监督计划明细取消定期/动态分组,监督原因支持手工填写
- 培训计划导入日期统一归一化为 yyyy-MM-dd
- 质量监控导入返回草稿计划并完善评价流程
- 标准库支持按标准方法查询已绑定的原始记录模板

Co-Authored-By: Claude Code <noreply@anthropic.com>
已修改15个文件
318 ■■■■■ 文件已修改
basic-server/src/main/java/com/ruoyi/basic/controller/StandardTreeController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
basic-server/src/main/java/com/ruoyi/basic/service/StandardTreeService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
basic-server/src/main/java/com/ruoyi/basic/service/impl/StandardTreeServiceImpl.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonTrainingDetailedServiceImpl.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/controller/QualitySuperviseController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/pojo/QualityMonitorDetailsEvaluate.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/pojo/QualitySuperviseDetailsRecord.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/service/QualityMonitorInspectionEvaluationService.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/service/QualityMonitorService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/service/QualitySuperviseService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/service/impl/QualityMonitorServiceImpl.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/service/impl/QualitySuperviseServiceImpl.java 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/task/QualitySuperviseSchedule.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/resources/mapper/QualitySuperviseDetailsRecordMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/resources/static/daily-supervise-record.docx 补丁 | 查看 | 原始文档 | blame | 历史
basic-server/src/main/java/com/ruoyi/basic/controller/StandardTreeController.java
@@ -193,6 +193,12 @@
        return Result.success(standardTreeService.selectTemplateBindings(standardTreeId));
    }
    @Operation(summary = "根据标准方法查询标准绑定的原始记录模板")
    @GetMapping("/selectTemplateBindingsByStandardMethod")
    public Result selectTemplateBindingsByStandardMethod(Integer standardMethodId) {
        return Result.success(standardTreeService.selectTemplateBindingsByStandardMethod(standardMethodId));
    }
    @Operation(summary = "保存标准绑定的原始记录模板")
    @PostMapping("/saveTemplateBindings")
    public Result saveTemplateBindings(@RequestBody StandardTreeTemplateBindingDto dto) {
basic-server/src/main/java/com/ruoyi/basic/service/StandardTreeService.java
@@ -43,6 +43,13 @@
    List<StandardTreeTemplateBindingVo> selectTemplateBindings(Integer standardTreeId);
    /**
     * 根据标准方法查询标准库中已绑定的原始记录模板
     * @param standardMethodId 标准方法ID
     * @return 已绑定的模板(多个标准库节点绑定的模板会去重合并)
     */
    List<StandardTreeTemplateBindingVo> selectTemplateBindingsByStandardMethod(Integer standardMethodId);
    boolean saveTemplateBindings(StandardTreeTemplateBindingDto dto);
    List<TemplateInspectionItemDto> selectTemplateInspectionItems(Integer standardTreeId);
basic-server/src/main/java/com/ruoyi/basic/service/impl/StandardTreeServiceImpl.java
@@ -419,6 +419,29 @@
    }
    @Override
    public List<StandardTreeTemplateBindingVo> selectTemplateBindingsByStandardMethod(Integer standardMethodId) {
        List<StandardTreeTemplateBindingVo> result = new ArrayList<>();
        if (standardMethodId == null) {
            return result;
        }
        List<StandardTree> trees = this.list(Wrappers.<StandardTree>lambdaQuery()
                .eq(StandardTree::getStandardMethodId, standardMethodId)
                .isNull(StandardTree::getSampleType)
                .isNull(StandardTree::getModel)
                .isNotNull(StandardTree::getSample)
                .orderByAsc(StandardTree::getId));
        Set<Integer> templateIds = new HashSet<>();
        for (StandardTree tree : trees) {
            for (StandardTreeTemplateBindingVo binding : selectTemplateBindings(tree.getId())) {
                if (templateIds.add(binding.getTemplateId())) {
                    result.add(binding);
                }
            }
        }
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean saveTemplateBindings(StandardTreeTemplateBindingDto dto) {
        if (dto == null || dto.getStandardTreeId() == null) {
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonTrainingDetailedServiceImpl.java
@@ -57,7 +57,7 @@
            personTrainingDetailed.setPlanId(planId);
            personTrainingDetailed.setState(3);
            personTrainingDetailed.setTrainingDate(i.getTrainingDate());
            personTrainingDetailed.setTrainingDate(normalizeTrainingDate(i.getTrainingDate()));
            personTrainingDetailedList.add(personTrainingDetailed);
@@ -68,6 +68,32 @@
        }
    }
    /**
     * 培训时间统一成 yyyy-MM-dd。
     * Excel 里被自动识别成日期的单元格读出来是 2026/3/15,手工录入的是 2026-03-15,
     * 这里统一转换,避免导出时格式不一致。
     */
    private String normalizeTrainingDate(String value) {
        if (value == null || value.isBlank()) {
            return value;
        }
        String text = value.trim().split("\\s+")[0]
                .replace('年', '-').replace('月', '-').replace("日", "")
                .replace('/', '-').replace('.', '-');
        String[] parts = text.split("-");
        if (parts.length == 3) {
            try {
                int year = Integer.parseInt(parts[0].trim());
                int month = Integer.parseInt(parts[1].trim());
                int day = Integer.parseInt(parts[2].trim());
                return String.format("%04d-%02d-%02d", year, month, day);
            } catch (NumberFormatException ignored) {
                // 不是可识别的日期格式,原样保存
            }
        }
        return value.trim();
    }
    @Override
    public void deleteAnnualPlanDetailTable(String ids) {
        String[] split = ids.split(",");
cnas-process/src/main/java/com/ruoyi/process/controller/QualitySuperviseController.java
@@ -191,6 +191,16 @@
    }
    /**
     * 被监督人员确认监督记录
     * @return
     */
    @Operation(summary = "被监督人员确认监督记录")
    @PostMapping("/confirmSuperviseRecord")
    public Result confirmSuperviseRecord(@RequestBody QualitySuperviseDetailsRecord qualitySuperviseDetailsRecord){
        return Result.success(qualitySuperviseService.confirmSuperviseRecord(qualitySuperviseDetailsRecord));
    }
    /**
     * 导出质量监督详情记录表
     * @return
     */
cnas-process/src/main/java/com/ruoyi/process/pojo/QualityMonitorDetailsEvaluate.java
@@ -29,10 +29,16 @@
    @Schema(description = "关联的质控检验任务ID")
    private Integer insOrderId;
    @Schema(description = "监督员用户ID,与评价人分开")
    @Schema(description = "监督员用户ID,默认为计划中指定的评价人")
    private Integer supervisorUserId;
    @Schema(description = "监督员名称,非数据库字段")
    @TableField(exist = false)
    private String supervisorName;
    @Schema(description = "被监督检测员用户ID")
    private Integer inspectorUserId;
    @Schema(description = "被监督检测员名称,非数据库字段")
    @TableField(exist = false)
    private String inspectorName;
    @Schema(description = "检测样品名称")
    private String sampleName;
    @Schema(description = "检测样品规格")
cnas-process/src/main/java/com/ruoyi/process/pojo/QualitySuperviseDetailsRecord.java
@@ -14,7 +14,8 @@
 * @since 2024-11-07
 */
@Data
@TableName("cnas_quality_supervise_details_record")
// autoResultMap: testing_process/supervision_evaluation 复用了历史列名, 需要 MP 自动生成结果映射
@TableName(value = "cnas_quality_supervise_details_record", autoResultMap = true)
@Schema(description = "质量监督详情记录表")
public class QualitySuperviseDetailsRecord {
@@ -51,8 +52,9 @@
    @Schema(description = "检测日期")
    private String testDate;
    @Schema(description = "检测过程")
    private String testingProcess;
    @Schema(description = "监督方式(多项顿号分隔):核查原始记录和报告、现场监督实际操作过程等")
    @TableField("testing_process")
    private String superviseMethod;
    @Schema(description = "情况记录")
    private String caseReload;
@@ -81,8 +83,9 @@
    @Schema(description = "检测报告")
    private String examiningReport;
    @Schema(description = "监督情况评价")
    private String supervisionEvaluation;
    @Schema(description = "被监督人员确认意见(程序4.2.5)")
    @TableField("supervision_evaluation")
    private String confirmOpinion;
    @Schema(description = "不合格处理意见")
    private String handlingAdvice;
cnas-process/src/main/java/com/ruoyi/process/service/QualityMonitorInspectionEvaluationService.java
@@ -61,6 +61,14 @@
            evaluation.setSampleName(detail.getSampleName());
            evaluation.setSampleSpec(detail.getSampleSpec());
        }
        // 评价记录里的“监督员”就是计划中指定的“评价人”,未保存过评价时直接反显评价人
        if (evaluation.getSupervisorUserId() == null) {
            evaluation.setSupervisorUserId(detail.getEvaluatorUserId());
        }
        // 历史评价记录可能没有保存被监督人员,回退到计划明细中的检测员
        if (evaluation.getInspectorUserId() == null) {
            evaluation.setInspectorUserId(detail.getInspectorUserId());
        }
        List<QualityMonitorEvaluateResult> rows = evaluation.getDetailsEvaluateId() == null
                ? List.of() : resultMapper.selectList(Wrappers.<QualityMonitorEvaluateResult>lambdaQuery()
                                                      .eq(QualityMonitorEvaluateResult::getDetailsEvaluateId, evaluation.getDetailsEvaluateId())
@@ -85,6 +93,15 @@
        }
        evaluation.setResultRows(rows);
        evaluation.setInspectionTaskState(task == null ? null : task.getInsState());
        // 监督员、检测员可能不在前端人员下拉列表中(角色或状态被过滤),回传名称保证能正常反显
        if (evaluation.getSupervisorUserId() != null) {
            var supervisor = userMapper.selectById(evaluation.getSupervisorUserId());
            if (supervisor != null) evaluation.setSupervisorName(supervisor.getName());
        }
        if (evaluation.getInspectorUserId() != null) {
            var inspector = userMapper.selectById(evaluation.getInspectorUserId());
            if (inspector != null) evaluation.setInspectorName(inspector.getName());
        }
        if (Integer.valueOf(1).equals(evaluation.getIsFinish())) {
            evaluation.setEvaluationEditReason("评价已批准归档,只能查看");
        } else if (task == null) {
@@ -117,9 +134,8 @@
            throw new ErrorException("只有指定评价人可以填写评价");
        if (input.getSupervisorUserId() == null
                || input.getSupervisorUserId().equals(detail.getInspectorUserId())
                || input.getSupervisorUserId().equals(detail.getEvaluatorUserId())
                || userMapper.selectById(input.getSupervisorUserId()) == null)
            throw new ErrorException("请选择与检测员、评价人不同的监督员");
            throw new ErrorException("请选择监督员(默认为计划评价人,不能与检测员相同)");
        Long pendingCount = stateMapper.selectCount(Wrappers.<InsOrderState>lambdaQuery()
                .eq(InsOrderState::getInsOrderId, task.getId()).ne(InsOrderState::getInsState, 5));
        if (pendingCount > 0) throw new ErrorException("原始记录复核通过后才能评价");
cnas-process/src/main/java/com/ruoyi/process/service/QualityMonitorService.java
@@ -30,9 +30,9 @@
    /**
     * 导入监控计划
     * @param file
     * @return
     * @return 导入后生成的草稿计划
     */
    boolean importQualityMonitor(MultipartFile file, QualityMonitor qualityMonitor);
    QualityMonitor importQualityMonitor(MultipartFile file, QualityMonitor qualityMonitor);
    /**
cnas-process/src/main/java/com/ruoyi/process/service/QualitySuperviseService.java
@@ -90,6 +90,13 @@
    boolean addSuperviseRecordOpinion(QualitySuperviseDetailsRecord qualitySuperviseDetailsRecord);
    /**
     * 被监督人员确认监督记录(程序4.2.5)
     * @param qualitySuperviseDetailsRecord 至少包含 superviseDetailsId 和 confirmOpinion
     * @return
     */
    boolean confirmSuperviseRecord(QualitySuperviseDetailsRecord qualitySuperviseDetailsRecord);
    /**
     * 导出监督记录表
     * @param superviseDetailsId
     * @param response
cnas-process/src/main/java/com/ruoyi/process/service/impl/QualityMonitorServiceImpl.java
@@ -211,7 +211,7 @@
     * @return
     */
    @Override
    public boolean importQualityMonitor(MultipartFile file, QualityMonitor monitor) {
    public QualityMonitor importQualityMonitor(MultipartFile file, QualityMonitor monitor) {
        // 当前登录用户
        Integer userId = SecurityUtils.getUserId().intValue();
        // 文件名称
@@ -226,39 +226,10 @@
        qualityMonitor.setWriteUserId(userId);
        qualityMonitor.setExamineUserId(monitor.getExamineUserId());
        qualityMonitor.setWriteTime(LocalDateTime.now());
        qualityMonitor.setWorkflowStatus("PENDING_REVIEW");
        // 导入只生成草稿,导入的质控项目还需要补全检测标准、检测员等信息;
        // 审核通知在编制人点击“提交审核”时统一发送。
        qualityMonitor.setWorkflowStatus("DRAFT");
        baseMapper.insert(qualityMonitor);
        User user = userMapper.selectById(userId);
        // 消息发送
        InformationNotification info = new InformationNotification();
        // 发送人
        info.setCreateUser(user.getName());
        info.setMessageType("6");
        info.setTheme("CNAS质量监控计划审核通知");
        info.setContent(fileName + "质量监控计划待审核");
        info.setSenderId(userId);
        // 接收人
        info.setConsigneeId(monitor.getExamineUserId());
        info.setJumpPath(MenuJumpPathConstants.QUALITY_MONITOR);
        informationNotificationService.addInformationNotification(info);
        // 发送企业微信通知
        threadPoolTaskExecutor.execute(() -> {
            // 查询发送人
            User people = userMapper.selectById(monitor.getExamineUserId());
            String message = "";
            message += "CNAS质量监控计划审核通知";
            message += "\n请去过程要求-质量监控计划";
            message += "\n" + fileName + "质量监控计划待审核";
            //发送企业微信消息通知
            try {
                WxCpUtils.inform(people.getAccount(), message, null);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
        List<QualityMonitorDetails> detailsUploadList = new ArrayList<>();
@@ -303,7 +274,7 @@
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return true;
        return qualityMonitor;
    }
    /**
cnas-process/src/main/java/com/ruoyi/process/service/impl/QualitySuperviseServiceImpl.java
@@ -56,6 +56,8 @@
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashSet;
@@ -178,8 +180,8 @@
                        throw new ErrorException("未找到被监督员:" + superviseDetails.getSupervisee());
                    }
                    superviseDetails.setSupervisedUserId(user.getId());
                    // 格式化时间
                    superviseDetails.setSuperviseTime(detailsUpload.getSuperviseTime());
                    // 统一日期格式,避免 Excel 把 2026-03-15 存成 2026/3/15 后无法解析
                    superviseDetails.setSuperviseTime(normalizeSuperviseTime(detailsUpload.getSuperviseTime()));
                    detailsUploadList.add(superviseDetails);
                }
@@ -432,6 +434,59 @@
    }
    /**
     * 统一监督日期格式为 yyyy.M:
     * 2026-03-15 / 2026/3/15 / 2026.3.15 / 2026年3月15日 -> 2026.3;
     * 已经是 2026.3 或 "3月、12月" 等写法保持原样。
     */
    private static String normalizeSuperviseTime(String superviseTime) {
        if (StringUtils.isBlank(superviseTime)) {
            return superviseTime;
        }
        String text = superviseTime.trim();
        Matcher matcher = Pattern.compile("^(\\d{4})\\s*[./年-]\\s*(\\d{1,2})(?:\\s*[./月-]\\s*(\\d{1,2})\\s*日?)?$")
                .matcher(text);
        if (matcher.matches()) {
            int month = Integer.parseInt(matcher.group(2));
            if (month >= 1 && month <= 12) {
                return matcher.group(1) + "." + month;
            }
        }
        return text;
    }
    /**
     * 解析监督日期对应的年月,兼容历史数据中的完整日期写法。
     */
    private static YearMonth parseSuperviseYearMonth(String superviseTime, String fallbackYear) {
        if (StringUtils.isNotBlank(superviseTime)) {
            String first = superviseTime.split("[、,,;;]+")[0].trim();
            Matcher fullDate = Pattern.compile("(\\d{4})\\s*[./年-]\\s*(\\d{1,2})").matcher(first);
            if (fullDate.find()) {
                int month = Integer.parseInt(fullDate.group(2));
                if (month >= 1 && month <= 12) {
                    return YearMonth.of(Integer.parseInt(fullDate.group(1)), month);
                }
            }
            // "3月、12月" 这类没有年份的写法,使用计划年度
            Matcher monthOnly = Pattern.compile("^(\\d{1,2})").matcher(first);
            if (monthOnly.find()) {
                int month = Integer.parseInt(monthOnly.group(1));
                if (month >= 1 && month <= 12) {
                    int year = LocalDate.now().getYear();
                    if (StringUtils.isNotBlank(fallbackYear)) {
                        Matcher yearMatcher = Pattern.compile("(\\d{4})").matcher(fallbackYear);
                        if (yearMatcher.find()) {
                            year = Integer.parseInt(yearMatcher.group(1));
                        }
                    }
                    return YearMonth.of(year, month);
                }
            }
        }
        return YearMonth.now();
    }
    /**
     * 监督频次: 按监督时间中的月份个数计算, 如 "2026.3" -> "1次/年", "3月、12月" -> "2次/年"
     */
    private static String buildSuperviseFrequency(String superviseTime) {
@@ -439,7 +494,7 @@
            return "";
        }
        int count = 0;
        for (String part : superviseTime.split("[、,,;;/\\s]+")) {
        for (String part : normalizeSuperviseTime(superviseTime).split("[、,,;;/\\s]+")) {
            if (part.matches(".*\\d.*")) {
                count++;
            }
@@ -456,7 +511,7 @@
        }
        StringBuilder sb = new StringBuilder();
        for (String part : superviseTime.split("[、,,;;]+")) {
            String s = part.trim();
            String s = normalizeSuperviseTime(part.trim());
            if (s.isEmpty()) {
                continue;
            }
@@ -491,9 +546,11 @@
        if (detailsRecord == null) {
            // 查询详情信息
            QualitySuperviseDetails qualitySuperviseDetails = qualitySuperviseDetailsMapper.selectById(superviseDetailsId);
            QualitySupervise qualitySupervise = baseMapper.selectById(qualitySuperviseDetails.getSuperviseId());
            // 查询上一个月第一条的记录信息, 判断被监督人去另外一个
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.M");
            YearMonth yearMonth = YearMonth.parse(qualitySuperviseDetails.getSuperviseTime(), formatter);
            // 监督日期兼容 2026.3 / 2026-03-15 / 2026/3/15 / 2026年3月15日 等写法
            YearMonth yearMonth = parseSuperviseYearMonth(qualitySuperviseDetails.getSuperviseTime(),
                    qualitySupervise == null ? null : qualitySupervise.getSuperviseYear());
            YearMonth previousYearMonth = yearMonth.minusMonths(1);
            LocalDate startDate = previousYearMonth.atDay(1);
@@ -515,21 +572,17 @@
            String supervisor = null;
            if (laseRecord != null) {
                // 查询计划获取另一个监督员
                QualitySupervise qualitySupervise = baseMapper.selectById(qualitySuperviseDetails.getSuperviseId());
                if (StringUtils.isNotBlank(qualitySupervise.getRecordUserIds())) {
                    List<String> recordUserIds = StrUtil.split(qualitySupervise.getRecordUserIds(), ',');
                    List<User> users = userMapper.selectList(Wrappers.<User>lambdaQuery()
                            .in(User::getId, recordUserIds));
                    if (CollectionUtils.isNotEmpty(users) && users.size() == 2) {
                        // 判断是否一样, 一样获取另外一个
                        if (users.get(0).getName().equals(laseRecord.getSupervisor())) {
                            supervisor = users.get(1).getName();
                        } else {
                            supervisor = users.get(0).getName();
                        }
            // 计划指定的监督员:首次填写时默认带出第一位;有上月记录时两位监督员交替
            if (qualitySupervise != null && StringUtils.isNotBlank(qualitySupervise.getRecordUserIds())) {
                List<String> recordUserIds = StrUtil.split(qualitySupervise.getRecordUserIds(), ',');
                List<User> users = userMapper.selectList(Wrappers.<User>lambdaQuery()
                        .in(User::getId, recordUserIds));
                if (CollectionUtils.isNotEmpty(users)) {
                    if (laseRecord != null && users.size() > 1
                            && users.get(0).getName().equals(laseRecord.getSupervisor())) {
                        supervisor = users.get(1).getName();
                    } else {
                        supervisor = users.get(0).getName();
                    }
                }
            }
@@ -643,6 +696,47 @@
    }
    /**
     * 被监督人员确认监督记录(程序4.2.5: 被监督对象对监督记录真实性提出确认意见)
     */
    @Override
    public boolean confirmSuperviseRecord(QualitySuperviseDetailsRecord input) {
        if (input == null || input.getSuperviseDetailsId() == null) {
            throw new ErrorException("缺少监督详细信息id");
        }
        QualitySuperviseDetailsRecord record = qualitySuperviseDetailsRecordMapper.selectOne(
                Wrappers.<QualitySuperviseDetailsRecord>lambdaQuery()
                        .eq(QualitySuperviseDetailsRecord::getSuperviseDetailsId, input.getSuperviseDetailsId())
                        .last("limit 1"));
        if (record == null) {
            throw new ErrorException("监督员尚未提交监督记录,无法确认");
        }
        Integer userId = SecurityUtils.getUserId().intValue();
        User user = userMapper.selectById(userId);
        if (user == null) {
            throw new ErrorException("当前用户不存在");
        }
        // 只有被监督人员本人才可以确认
        boolean isSupervisee = StrUtil.split(record.getTestMember(), '、').stream()
                .map(String::trim)
                .anyMatch(name -> name.equals(user.getName()));
        if (!isSupervisee) {
            QualitySuperviseDetails detail = qualitySuperviseDetailsMapper.selectById(input.getSuperviseDetailsId());
            isSupervisee = detail != null && userId.equals(detail.getSupervisedUserId());
        }
        if (!isSupervisee) {
            throw new ErrorException("只有被监督人员可以确认监督记录");
        }
        String opinion = StringUtils.isBlank(input.getConfirmOpinion())
                ? "对监督记录真实性表示认可。" : input.getConfirmOpinion().trim();
        qualitySuperviseDetailsRecordMapper.update(null,
                Wrappers.<QualitySuperviseDetailsRecord>lambdaUpdate()
                        .eq(QualitySuperviseDetailsRecord::getSuperviseDetailsRecordId,
                                record.getSuperviseDetailsRecordId())
                        .set(QualitySuperviseDetailsRecord::getConfirmOpinion, opinion));
        return true;
    }
    /**
     * 导出监督记录表(日常监督检查记录表)
     * @param superviseDetailsId
     * @param response
@@ -670,6 +764,11 @@
                    put("qualityUrl", signatureOrTransparent(recordDto.getRatifyUserId()));
                    put("qualityDateStr", recordDto.getRatifyTime() != null ?
                            recordDto.getRatifyTime().format(RECORD_DATE_FORMATTER) : "");
                    // 程序4.2.5: 被监督人员对监督记录真实性的确认意见, 未确认时沿用纸张表的默认表述
                    put("confirmOpinionText", StringUtils.isNotBlank(recordDto.getConfirmOpinion())
                            ? recordDto.getConfirmOpinion() : "对监督记录真实性表示认可。");
                    put("superviseMethodText", StringUtils.isNotBlank(recordDto.getSuperviseMethod())
                            ? recordDto.getSuperviseMethod() : "");
                }});
        try {
@@ -690,7 +789,7 @@
    }
    /**
     * 监督内容(9项, 按纸张表横排顺序)
     * 监督内容(9项, 与受控记录表 LMSY-CX29-JL02 的横排顺序、文字完全一致)
     */
    private static final String[] SUPERVISE_CONTENT_ITEMS = {
            "人员持证及培训", "设施及环境条件", "检测方法",
cnas-process/src/main/java/com/ruoyi/process/task/QualitySuperviseSchedule.java
@@ -91,8 +91,9 @@
        List<QualitySuperviseDetails> qualitySuperviseDetails = qualitySuperviseDetailsMapper.selectList(Wrappers.<QualitySuperviseDetails>lambdaQuery()
                .eq(QualitySuperviseDetails::getSuperviseTime, format));
        // 根据定期监督和动态监督区分
        Map<String, List<QualitySuperviseDetails>> superviseReasonMap = qualitySuperviseDetails.stream().collect(Collectors.groupingBy(QualitySuperviseDetails::getSuperviseReason));
        // 根据监督原因区分(定期监督/动态监督), 监督原因允许手工填写或为空, 这里做空值兜底
        Map<String, List<QualitySuperviseDetails>> superviseReasonMap = qualitySuperviseDetails.stream()
                .collect(Collectors.groupingBy(details -> StrUtil.nullToEmpty(details.getSuperviseReason())));
        // 获取当月日期
        DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy年MM月");
cnas-process/src/main/resources/mapper/QualitySuperviseDetailsRecordMapper.xml
@@ -4,7 +4,10 @@
    <!--导出监督记录表-->
    <select id="selectSuperviseDetailRecord" resultType="com.ruoyi.process.pojo.QualitySuperviseDetailsRecord">
        select cqsdr.*
        <!-- 复用历史列 testing_process / supervision_evaluation,resultType 自动映射需要别名 -->
        select cqsdr.*,
               cqsdr.testing_process as supervise_method,
               cqsdr.supervision_evaluation as confirm_opinion
        from cnas_quality_supervise_details_record cqsdr
        where cqsdr.supervise_details_id = #{superviseDetailsId}
    </select>
cnas-process/src/main/resources/static/daily-supervise-record.docx
Binary files differ