gaoluyang
昨天 abd339106ed3a6465293edccd33e5f87bd43bdde
feat: 质量监控/人员培训对齐程序文件,体系文件控制附件格式放开

- 质量监控:质控方式改为8种质控措施多选,统一记录名称与导出文件名
- 人员培训:支持外聘讲师,修复培训记录导出签名缺失导致的失败
- 体系文件控制:附件支持常见办公格式,修复上传/下载路径,审核预览支持Word和图片转PDF
- 模板:质量监控三个Word模板按程序文件更新

Co-Authored-By: Claude Code <noreply@anthropic.com>
已修改12个文件
已添加1个文件
262 ■■■■■ 文件已修改
cnas-manage/src/main/java/com/ruoyi/manage/service/impl/ManageDocumentAlterServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/java/com/ruoyi/manage/service/impl/ManageDocumentControlledServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/java/com/ruoyi/manage/service/impl/ManageDocumentListServiceImpl.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/java/com/ruoyi/manage/util/DocumentPreviewUtils.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/controller/PersonTrainingController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/pojo/PersonTrainingDetailed.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonTrainingDetailedServiceImpl.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonTrainingServiceImpl.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/resources/mapper/PersonTrainingDetailedMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/java/com/ruoyi/process/service/impl/QualityMonitorServiceImpl.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/resources/static/quality-monitor-details-ratify.docx 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/resources/static/quality-monitor-evaluate.docx 补丁 | 查看 | 原始文档 | blame | 历史
cnas-process/src/main/resources/static/quality-monitor.docx 补丁 | 查看 | 原始文档 | blame | 历史
cnas-manage/src/main/java/com/ruoyi/manage/service/impl/ManageDocumentAlterServiceImpl.java
@@ -13,7 +13,9 @@
import com.ruoyi.common.utils.QueryWrappers;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.system.mapper.UserMapper;
import com.ruoyi.manage.util.DocumentPreviewUtils;
import com.ruoyi.manage.mapper.*;
@@ -239,21 +241,11 @@
    @Override
    public void checkManageDocumentAlterPdf(Long id, HttpServletResponse response) throws Exception {
        ManageDocumentAlter manageDocumentAlter = manageDocumentAlterMapper.selectById(id);
        File file = new File(wordUrl + "/" + manageDocumentAlter.getAlterAfterUrl());
        FileInputStream fileInputStream = new FileInputStream(file);
        response.setContentType("application/pdf");
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-disposition", "inline;filename=" + file.getName());
        response.setContentLength((int)file.length());
        OutputStream os = response.getOutputStream();
        // å°†æ–‡ä»¶å†…容写入输出流
        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = fileInputStream.read(buffer)) != -1) {
            os.write(buffer, 0, bytesRead);
        if (manageDocumentAlter == null || ObjectUtils.isEmpty(manageDocumentAlter.getAlterAfterUrl())) {
            throw new ErrorException("文件未上传");
        }
        os.flush();
        os.close();
        // PDF ç›´æŽ¥è¾“出,Word/图片在线转 PDF,供前端预览盖章
        DocumentPreviewUtils.writeAsPdf(new File(wordUrl + "/" + manageDocumentAlter.getAlterAfterUrl()), response);
    }
    @Override
cnas-manage/src/main/java/com/ruoyi/manage/service/impl/ManageDocumentControlledServiceImpl.java
@@ -8,6 +8,7 @@
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.core.domain.entity.User;
import com.ruoyi.common.utils.QueryWrappers;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.manage.mapper.ManageDocumentControlledMapper;
import com.ruoyi.manage.mapper.ManageDocumentListMapper;
import com.ruoyi.manage.mapper.ManageRecordCheckMapper;
@@ -15,6 +16,7 @@
import com.ruoyi.manage.pojo.ManageDocumentList;
import com.ruoyi.manage.pojo.ManageRecordCheck;
import com.ruoyi.manage.service.ManageDocumentControlledService;
import com.ruoyi.manage.util.DocumentPreviewUtils;
import com.ruoyi.system.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@@ -199,21 +201,11 @@
    @Override
    public void checkManageDocumentControlledPdf(Long id, HttpServletResponse response) throws Exception {
        ManageDocumentControlled manageDocumentControlled = manageDocumentControlledMapper.selectById(id);
        File file = new File(wordUrl + "/" + manageDocumentControlled.getUrl());
        FileInputStream fileInputStream = new FileInputStream(file);
        response.setContentType("application/pdf");
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-disposition", "inline;filename=" + file.getName());
        response.setContentLength((int)file.length());
        OutputStream os = response.getOutputStream();
        // å°†æ–‡ä»¶å†…容写入输出流
        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = fileInputStream.read(buffer)) != -1) {
            os.write(buffer, 0, bytesRead);
        if (manageDocumentControlled == null || ObjectUtils.isEmpty(manageDocumentControlled.getUrl())) {
            throw new ErrorException("文件未上传");
        }
        os.flush();
        os.close();
        // PDF ç›´æŽ¥è¾“出,Word/图片在线转 PDF,供前端预览盖章
        DocumentPreviewUtils.writeAsPdf(new File(wordUrl + "/" + manageDocumentControlled.getUrl()), response);
    }
}
cnas-manage/src/main/java/com/ruoyi/manage/service/impl/ManageDocumentListServiceImpl.java
@@ -11,6 +11,7 @@
import com.ruoyi.common.utils.QueryWrappers;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.manage.mapper.ManageDocumentListMapper;
import com.ruoyi.manage.pojo.ManageDocumentList;
import com.ruoyi.manage.service.ManageDocumentListService;
@@ -25,6 +26,8 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -58,29 +61,29 @@
    @Override
    public int uploadFile(Integer id, MultipartFile file) {
        String urlString;
        String pathName;
        String path;
        ManageDocumentList manageDocumentList = manageDocumentListMapper.selectById(id);
        if (ObjectUtils.isNotEmpty(manageDocumentList.getUrl())){
            // åˆ é™¤æ—§æ–‡ä»¶
            File oldFile = new File(wordUrl + "/" + manageDocumentList.getUrl());
            oldFile.delete();
        if (manageDocumentList == null) {
            throw new ErrorException("文件清单不存在");
        }
        //上传新文件
        path = wordUrl;
        if (file == null || file.isEmpty()) {
            throw new ErrorException("请选择要上传的文件");
        }
        File realpath = new File(wordUrl).getAbsoluteFile();
        try {
            File realpath = new File(path);
            if (!realpath.exists()) {
                realpath.mkdirs();
            if (!realpath.exists() && !realpath.mkdirs()) {
                throw new IOException("目录创建失败: " + realpath);
            }
            pathName = UUID.randomUUID() + "_" + file.getOriginalFilename();
            urlString = realpath + "/" + pathName;
            file.transferTo(new File(urlString));
            // ç”¨ Files.copy è€Œä¸æ˜¯ transferTo:transferTo é‡åˆ°ç›¸å¯¹è·¯å¾„会按 Tomcat ä¸´æ—¶ç›®å½•解析
            Files.copy(file.getInputStream(), new File(realpath, pathName).toPath(), StandardCopyOption.REPLACE_EXISTING);
        } catch (Exception e) {
            e.printStackTrace();
            System.err.println("附件上传错误");
            return 0;
            throw new ErrorException("附件上传错误: " + e.getMessage());
        }
        if (ObjectUtils.isNotEmpty(manageDocumentList.getUrl())) {
            // åˆ é™¤æ—§æ–‡ä»¶
            new File(realpath, manageDocumentList.getUrl()).delete();
        }
        manageDocumentList.setUrl(pathName);
        return manageDocumentListMapper.updateById(manageDocumentList);
cnas-manage/src/main/java/com/ruoyi/manage/util/DocumentPreviewUtils.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
package com.ruoyi.manage.util;
import com.aspose.words.Document;
import com.aspose.words.DocumentBuilder;
import com.aspose.words.License;
import com.aspose.words.SaveFormat;
import com.ruoyi.framework.exception.ErrorException;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.io.ClassPathResource;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
/**
 * ä½“系文件附件预览工具:把附件统一以 PDF æµè¾“出给前端预览/盖章。
 * PDF åŽŸæ ·è¾“å‡ºï¼›Word、图片用 Aspose.Words åœ¨çº¿è½¬ PDF;Excel/PPT ç­‰æ— æ³•转 PDF çš„æ ¼å¼ç»™å‡ºæ˜Žç¡®æç¤ºã€‚
 */
public final class DocumentPreviewUtils {
    private static final String[] WORD_EXT = {".doc", ".docx", ".rtf", ".txt", ".wps"};
    private static final String[] IMAGE_EXT = {".png", ".jpg", ".jpeg", ".bmp", ".gif"};
    private DocumentPreviewUtils() {
    }
    public static void writeAsPdf(File file, HttpServletResponse response) throws Exception {
        if (file == null || !file.exists()) {
            throw new ErrorException("附件不存在或已被删除");
        }
        String name = file.getName().toLowerCase();
        if (name.endsWith(".pdf")) {
            prepare(response, file.getName(), file.length());
            try (InputStream in = new FileInputStream(file);
                 OutputStream os = response.getOutputStream()) {
                in.transferTo(os);
                os.flush();
            }
            return;
        }
        Document doc;
        if (endsWithAny(name, WORD_EXT)) {
            applyLicense();
            doc = new Document(file.getPath());
        } else if (endsWithAny(name, IMAGE_EXT)) {
            applyLicense();
            doc = new Document();
            new DocumentBuilder(doc).insertImage(file.getPath());
        } else {
            throw new ErrorException("该文件格式不支持在线盖章预览,请上传 PDF æˆ– Word æ–‡ä»¶");
        }
        // å…ˆè½¬æ¢åˆ°å†…存,转换失败时响应头还没提交,前端才能收到错误信息
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        doc.save(buffer, SaveFormat.PDF);
        byte[] bytes = buffer.toByteArray();
        prepare(response, file.getName() + ".pdf", bytes.length);
        try (OutputStream os = response.getOutputStream()) {
            os.write(bytes);
            os.flush();
        }
    }
    private static void prepare(HttpServletResponse response, String fileName, long length) {
        response.setContentType("application/pdf");
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-disposition", "inline;filename=" + fileName);
        response.setContentLength((int) length);
    }
    private static boolean endsWithAny(String name, String[] suffixes) {
        for (String suffix : suffixes) {
            if (name.endsWith(suffix)) {
                return true;
            }
        }
        return false;
    }
    private static void applyLicense() throws Exception {
        // å‡­è¯ï¼Œå¦åˆ™è½¬æ¢åŽå¸¦æ°´å°
        try (InputStream is = new ClassPathResource("lib/license.xml").getInputStream()) {
            License license = new License();
            license.setLicense(is);
        }
    }
}
cnas-personnel/src/main/java/com/ruoyi/personnel/controller/PersonTrainingController.java
@@ -96,8 +96,17 @@
    public Result<?> addOrUpdatePersonTrainingDetailed(@RequestBody PersonTrainingDetailed personTrainingDetailed) {
        if (ObjectUtils.isEmpty(personTrainingDetailed.getId())) {
            personTrainingDetailed.setState(3);
            personTrainingDetailedService.save(personTrainingDetailed);
            return Result.success();
        }
        personTrainingDetailedService.saveOrUpdate(personTrainingDetailed);
        // è®²å¸ˆå¯èƒ½åœ¨å†…部人员和外聘人员之间切换,先显式覆盖两列,避免 updateById è·³è¿‡ null å¯¼è‡´æ—§å€¼æ®‹ç•™
        personTrainingDetailedService.update(null, Wrappers.<PersonTrainingDetailed>lambdaUpdate()
                .eq(PersonTrainingDetailed::getId, personTrainingDetailed.getId())
                .set(PersonTrainingDetailed::getTrainingLecturerId, personTrainingDetailed.getTrainingLecturerId())
                .set(PersonTrainingDetailed::getTrainingLecturerName, personTrainingDetailed.getTrainingLecturerName()));
        personTrainingDetailed.setTrainingLecturerId(null);
        personTrainingDetailed.setTrainingLecturerName(null);
        personTrainingDetailedService.updateById(personTrainingDetailed);
        return Result.success();
    }
cnas-personnel/src/main/java/com/ruoyi/personnel/pojo/PersonTrainingDetailed.java
@@ -56,6 +56,9 @@
    @Schema(description = "培训讲师_id")
    private Integer trainingLecturerId;
    @Schema(description = "外聘/非系统讲师姓名;内部讲师仍存 trainingLecturerId")
    private String trainingLecturerName;
    @Schema(description = "计划培训日期")
    private String trainingDate;
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonTrainingDetailedServiceImpl.java
@@ -2,7 +2,7 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -47,13 +47,20 @@
            PersonTrainingDetailed personTrainingDetailed = new PersonTrainingDetailed();
            BeanUtils.copyProperties(i, personTrainingDetailed);
            // åŒ¹é…è®²å¸ˆ
            User user = userMapper.selectOne(Wrappers.<User>lambdaQuery()
                    .eq(User::getName, i.getTrainingLecturerName()));
            if (ObjectUtils.isEmpty(user)) {
                throw new ErrorException("未找到该讲师:" + i.getTrainingLecturerName());
            // åŒ¹é…è®²å¸ˆï¼šç³»ç»Ÿå†…唯一匹配的存用户ID;外聘/非系统讲师按姓名文本保存
            if (StringUtils.isBlank(i.getTrainingLecturerName())) {
                throw new ErrorException("培训讲师不能为空");
            }
            personTrainingDetailed.setTrainingLecturerId(user.getId());
            String lecturerName = i.getTrainingLecturerName().trim();
            List<User> lecturers = userMapper.selectList(Wrappers.<User>lambdaQuery()
                    .eq(User::getName, lecturerName));
            if (lecturers.size() == 1) {
                personTrainingDetailed.setTrainingLecturerId(lecturers.get(0).getId());
                personTrainingDetailed.setTrainingLecturerName(null);
            } else {
                personTrainingDetailed.setTrainingLecturerId(null);
                personTrainingDetailed.setTrainingLecturerName(lecturerName);
            }
            personTrainingDetailed.setPlanId(planId);
            personTrainingDetailed.setState(3);
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonTrainingServiceImpl.java
@@ -19,6 +19,7 @@
import com.ruoyi.common.utils.WxCpUtils;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.inspect.util.HackLoopTableRenderPolicy;
import com.ruoyi.inspect.util.UserUtils;
import com.ruoyi.personnel.dto.PersonTrainingDetailedDto;
import com.ruoyi.personnel.dto.PersonTrainingDto;
import com.ruoyi.personnel.dto.PersonTrainingRecordDto;
@@ -131,11 +132,14 @@
            message += "CNAS培训计划审核通知";
            message += "\n请去资源管理-人员-培训计划填写";
            message += "\n" + fileName + "的培训计划待审核";
            //发送企业微信消息通知
            //发送企业微信消息通知(接收人不存在或发送失败不影响导入流程)
            if (personnel == null || StringUtils.isBlank(personnel.getAccount())) {
                return;
            }
            try {
                WxCpUtils.inform(personnel.getAccount(), message, null);
            } catch (Exception e) {
                throw new RuntimeException(e);
                e.printStackTrace();
            }
        });
@@ -306,9 +310,10 @@
        // æŸ¥è¯¢äººå‘˜åŸ¹è®­æ˜Žç»†
        PersonTrainingDetailedDto detailedDto = personTrainingDetailedMapper.selectTrainingDetail(id);
        // ç”ŸæˆåŸ¹è®­è€å¸ˆç­¾å
        detailedDto.setTrainingLecturerRender(StringUtils.isNotBlank(detailedDto.getTrainingLecturerSignatureUrl())
                    ? Pictures.ofLocal(imgUrl + "/" + detailedDto.getTrainingLecturerSignatureUrl()).create() : null);
        // ç”ŸæˆåŸ¹è®­è€å¸ˆç­¾åï¼ˆæœªé…ç½®ç­¾åæ—¶ç”¨é€æ˜Žå›¾ç‰‡å…œåº•,模板里的图片占位不能为 null)
        detailedDto.setTrainingLecturerRender(UserUtils.orTransparentPicture(
                StringUtils.isNotBlank(detailedDto.getTrainingLecturerSignatureUrl())
                        ? Pictures.ofLocal(imgUrl + "/" + detailedDto.getTrainingLecturerSignatureUrl()).create() : null));
        // æŸ¥è¯¢åŸ¹è®­çš„人员
        List<PersonTrainingRecordDto> recordDtos = personTrainingRecordMapper.selectListByTrainingDetailedId(id);
@@ -320,15 +325,15 @@
        for (PersonTrainingRecordDto recordDto : recordDtos) {
            switch (count) {
                case 0:
                    exportDto.setUserNameRender1(StringUtils.isNotBlank(recordDto.getSignatureUrl())
                            ? Pictures.ofLocal(imgUrl + "/" + recordDto.getSignatureUrl()).create() : null);
                    exportDto.setUserNameRender1(UserUtils.orTransparentPicture(StringUtils.isNotBlank(recordDto.getSignatureUrl())
                            ? Pictures.ofLocal(imgUrl + "/" + recordDto.getSignatureUrl()).create() : null));
                    exportDto.setDepartment1(recordDto.getDepartment());
                    exportDto.setExaminationResults1(recordDto.getExaminationResults());
                    count ++;
                    break;
                case 1:
                    exportDto.setUserNameRender2(StringUtils.isNotBlank(recordDto.getSignatureUrl())
                            ? Pictures.ofLocal(imgUrl + "/" + recordDto.getSignatureUrl()).create() : null);
                    exportDto.setUserNameRender2(UserUtils.orTransparentPicture(StringUtils.isNotBlank(recordDto.getSignatureUrl())
                            ? Pictures.ofLocal(imgUrl + "/" + recordDto.getSignatureUrl()).create() : null));
                    exportDto.setDepartment2(recordDto.getDepartment());
                    exportDto.setExaminationResults2(recordDto.getExaminationResults());
                    exportDtoList.add(exportDto);
@@ -337,14 +342,17 @@
                    break;
            }
        }
        // åªæœ‰å•数条记录时,最后一行才有数据;偶数条时 exportDto æ˜¯ç©ºå¯¹è±¡ï¼Œä¸èƒ½åŠ è¿›è¡¨æ ¼
        if (count != 0) {
        exportDtoList.add(exportDto);
        }
        // è´¨é‡è´Ÿè´£äºº
        // è´¨é‡è´Ÿè´£äººï¼ˆæœªç­¾åæ—¶ç”¨é€æ˜Žå›¾ç‰‡å…œåº•)
        String assessmentUserUrl = null;
        if (detailedDto.getAssessmentUserId() != null) {
            assessmentUserUrl = userMapper.selectById(detailedDto.getAssessmentUserId()).getSignatureUrl();
            if (StringUtils.isBlank(assessmentUserUrl)) {
                throw new ErrorException("找不到评价人的签名");
            User assessmentUser = userMapper.selectById(detailedDto.getAssessmentUserId());
            if (assessmentUser != null) {
                assessmentUserUrl = assessmentUser.getSignatureUrl();
            }
        }
@@ -359,7 +367,8 @@
                new HashMap<String, Object>() {{
                    put("trainingDetail", detailedDto);
                    put("trainingRecordsList", exportDtoList);
                    put("assessmentUserUrl", StringUtils.isNotBlank(finalAssessmentUserUrl) ? Pictures.ofLocal(imgUrl + "/" + finalAssessmentUserUrl).create() : null);
                    put("assessmentUserUrl", UserUtils.orTransparentPicture(StringUtils.isNotBlank(finalAssessmentUserUrl)
                            ? Pictures.ofLocal(imgUrl + "/" + finalAssessmentUserUrl).create() : null));
                }});
        try {
            response.setContentType("application/msword");
cnas-personnel/src/main/resources/mapper/PersonTrainingDetailedMapper.xml
@@ -4,7 +4,7 @@
    <select id="queryTheAnnualPlanDetailsTable" resultType="com.ruoyi.personnel.dto.PersonTrainingDetailedDto">
        SELECT cptd.*,
        u1.name training_lecturer_name,
        COALESCE(u1.name, cptd.training_lecturer_name) training_lecturer_name,
        dl.name holding_department_name, !isnull(cptr.training_record_id) whether_claim
        FROM cnas_person_training_detailed cptd
        left join user u1 on cptd.training_lecturer_id = u1.id
@@ -18,7 +18,7 @@
                and u1.id = #{userId}
            </if>
            <if test="trainingLecturerName != null and trainingLecturerName != ''">
                and u1.name like concat('%', #{trainingLecturerName}, '%')
                and COALESCE(u1.name, cptd.training_lecturer_name) like concat('%', #{trainingLecturerName}, '%')
            </if>
            <if test="trainingDate != null and trainingDate != ''">
                and date_format(cptd.training_date,'%Y-%m-%d') = #{trainingDate}
@@ -29,7 +29,7 @@
    <!-- æ ¹æ®ä¸»è¡¨id查询详情 -->
    <select id="selectTrainingList" resultType="com.ruoyi.personnel.dto.PersonTrainingDetailedDto">
        SELECT cptd.*,
               u1.name training_lecturer_name
               COALESCE(u1.name, cptd.training_lecturer_name) training_lecturer_name
        FROM cnas_person_training_detailed cptd
                 left join user u1 on cptd.training_lecturer_id = u1.id
        where cptd.plan_id = #{trainingId}
@@ -38,7 +38,7 @@
    <!-- æŸ¥è¯¢è¯¦ç»† -->
    <select id="selectTrainingDetail" resultType="com.ruoyi.personnel.dto.PersonTrainingDetailedDto">
        SELECT cptd.*,
               u1.name training_lecturer_name,
               COALESCE(u1.name, cptd.training_lecturer_name) training_lecturer_name,
               u1.signature_url training_lecturer_signature_url
        FROM cnas_person_training_detailed cptd
                 left join user u1 on cptd.training_lecturer_id = u1.id
cnas-process/src/main/java/com/ruoyi/process/service/impl/QualityMonitorServiceImpl.java
@@ -248,6 +248,7 @@
                        // å¯¹è±¡å¤åˆ¶
                    QualityMonitorDetails monitorDetails = new QualityMonitorDetails();
                    BeanUtils.copyProperties(detailsUpload, monitorDetails);
                    monitorDetails.setMonitorWay(normalizeMonitorWay(detailsUpload.getMonitorWay()));
                    monitorDetails.setQualityMonitorId(qualityMonitor.getQualityMonitorId());
                    if (!detailsUpload.getCompletionMonth().matches("\\d{4}-\\d{2}")) {
                        throw new ErrorException("完成时限格式应为yyyy-MM");
@@ -275,6 +276,20 @@
            throw new RuntimeException(e);
        }
        return qualityMonitor;
    }
    /**
     * è´¨æŽ§æ–¹å¼ç»Ÿä¸€ç”¨â€œã€â€åˆ†éš”,兼容导入模板里常见的逗号/分号写法。
     */
    private String normalizeMonitorWay(String value) {
        if (StringUtils.isBlank(value)) {
            return value;
        }
        return java.util.Arrays.stream(value.split("[、,,;;]"))
                .map(String::trim)
                .filter(StringUtils::isNotBlank)
                .distinct()
                .collect(Collectors.joining("、"));
    }
    /**
@@ -628,7 +643,7 @@
        try {
            response.setContentType("application/msword");
            String fileName = URLEncoder.encode(
                    "质量监控实施计划", "UTF-8");
                    "质量控制措施", "UTF-8");
            response.setHeader("Content-disposition",
                    "attachment;filename=" + fileName + ".docx");
            OutputStream os = response.getOutputStream();
@@ -842,7 +857,7 @@
        try {
            response.setContentType("application/msword");
            String fileName = URLEncoder.encode(
                    "监控评价", "UTF-8");
                    "质量控制评价记录", "UTF-8");
            response.setHeader("Content-disposition",
                    "attachment;filename=" + fileName + ".docx");
            OutputStream os = response.getOutputStream();
cnas-process/src/main/resources/static/quality-monitor-details-ratify.docx
Binary files differ
cnas-process/src/main/resources/static/quality-monitor-evaluate.docx
Binary files differ
cnas-process/src/main/resources/static/quality-monitor.docx
Binary files differ