吴羡
2026-10-21 cf4fbcbf19c7d326611598c2fc90a4aeb354c4a5
Merge remote-tracking branch 'origin/master'
已添加1个文件
已修改32个文件
852 ■■■■■ 文件已修改
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 | 历史
basic-server/src/main/java/com/ruoyi/basic/utils/StandardTemplateInspectionItemParser.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/java/com/ruoyi/personnel/service/impl/PersonTrainingServiceImpl.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-personnel/src/main/resources/mapper/PersonTrainingDetailedMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | 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 58 ●●●●● 补丁 | 查看 | 原始文档 | 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 | 历史
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 | 历史
inspect-server/src/main/java/com/ruoyi/inspect/pojo/InsOrder.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/pojo/InsOrderStandardTemplate.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderServiceImpl.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsProductServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsSampleReceiptServiceImpl.java 89 ●●●●● 补丁 | 查看 | 原始文档 | 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) {
basic-server/src/main/java/com/ruoyi/basic/utils/StandardTemplateInspectionItemParser.java
@@ -13,9 +13,10 @@
import java.util.List;
import java.util.Set;
/** è§£æž Luckysheet åŽŸå§‹è®°å½•æ¨¡æ¿ä¸­çš„â€œæ£€éªŒé¡¹â€æ ‡è®°ã€‚ */
/** è§£æž Luckysheet åŽŸå§‹è®°å½•æ¨¡æ¿ä¸­çš„â€œæ£€éªŒé¡¹â€ï¼›æœªæ ‡è®°æ—¶å›žé€€åˆ°â€œå¯¼å‡ºå€¼â€è¡Œã€‚ */
public final class StandardTemplateInspectionItemParser {
    private static final String INSPECTION_ITEM_MARK = "检验项";
    private static final String EXPORT_VALUE_MARK = "导出值";
    private StandardTemplateInspectionItemParser() {
    }
@@ -34,11 +35,22 @@
        } catch (Exception e) {
            throw new BaseException("原始记录模板格式错误");
        }
        List<TemplateInspectionItemDto> result = new ArrayList<>();
        Set<Integer> rows = new HashSet<>();
        if (sheets == null) {
            throw new BaseException("原始记录模板格式错误");
        }
        List<TemplateInspectionItemDto> result = parseMarkedCells(sheets, templateId, templateSort,
                INSPECTION_ITEM_MARK);
        if (result.isEmpty()) {
            result = parseMarkedCells(sheets, templateId, templateSort, EXPORT_VALUE_MARK);
        }
        result.sort(Comparator.comparing(TemplateInspectionItemDto::getTemplateRowIndex));
        return result;
    }
    private static List<TemplateInspectionItemDto> parseMarkedCells(JSONArray sheets, Integer templateId,
                                                                     Integer templateSort, String mark) {
        List<TemplateInspectionItemDto> result = new ArrayList<>();
        Set<Integer> rows = new HashSet<>();
        for (Object sheetObject : sheets) {
            JSONObject sheet = (JSONObject) sheetObject;
            JSONArray cells = sheet.getJSONArray("celldata");
@@ -48,16 +60,19 @@
            for (Object cellObject : cells) {
                JSONObject cell = (JSONObject) cellObject;
                JSONObject value = cell.getJSONObject("v");
                if (value == null || !isInspectionItem(value)) {
                if (value == null || !hasMark(value, mark)) {
                    continue;
                }
                Integer row = cell.getInteger("r");
                String item = getCellText(value);
                if (row == null || !StringUtils.hasText(item)) {
                    throw new BaseException("“检验项”标记所在单元格必须填写检验项名称");
                    throw new BaseException("“" + mark + "”标记所在单元格必须填写检验项名称");
                }
                if (!rows.add(row)) {
                    throw new BaseException("同一模板行只能配置一个“检验项”标记");
                    if (EXPORT_VALUE_MARK.equals(mark)) {
                        continue;
                    }
                    throw new BaseException("同一模板行只能配置一个“" + mark + "”标记");
                }
                TemplateInspectionItemDto dto = new TemplateInspectionItemDto();
                dto.setTemplateId(templateId);
@@ -67,7 +82,6 @@
                result.add(dto);
            }
        }
        result.sort(Comparator.comparing(TemplateInspectionItemDto::getTemplateRowIndex));
        return result;
    }
@@ -101,8 +115,8 @@
        return richText.toString();
    }
    private static boolean isInspectionItem(JSONObject value) {
    private static boolean hasMark(JSONObject value, String mark) {
        JSONObject ps = value.getJSONObject("ps");
        return ps != null && INSPECTION_ITEM_MARK.equals(ps.getString("value"));
        return ps != null && mark.equals(ps.getString("value"));
    }
}
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,17 +47,24 @@
            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);
            personTrainingDetailed.setTrainingDate(i.getTrainingDate());
            personTrainingDetailed.setTrainingDate(normalizeTrainingDate(i.getTrainingDate()));
            personTrainingDetailedList.add(personTrainingDetailed);
@@ -68,6 +75,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-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;
            }
        }
        exportDtoList.add(exportDto);
        // åªæœ‰å•数条记录时,最后一行才有数据;偶数条时 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/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<>();
@@ -277,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");
@@ -303,7 +275,21 @@
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return true;
        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("、"));
    }
    /**
@@ -657,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();
@@ -871,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/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
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
inspect-server/src/main/java/com/ruoyi/inspect/pojo/InsOrder.java
@@ -42,6 +42,9 @@
    @Schema(description = "质量监控计划明细ID")
    private Integer qualityMonitorDetailsId;
    @Schema(description = "新聚直绑模板下单时选择的标准库二级节点ID")
    private Integer standardTreeId;
    @Schema(description = "下单客户")
    private String custom;
inspect-server/src/main/java/com/ruoyi/inspect/pojo/InsOrderStandardTemplate.java
@@ -64,6 +64,9 @@
    @Schema(description = "原始记录已填写单元格,按坐标保存")
    private String recordValues;
    @Schema(description = "默认设备是否已在首次进入检验任务时初始化:0否,1是")
    private Integer deviceDefaultsInitialized;
    @Schema(description = "该模板检验时的温度")
    private String temperature;
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java
@@ -1659,6 +1659,8 @@
            refreshed.setRemark(standardTemplate == null ? oldSnapshot.getRemark() : standardTemplate.getRemark());
            refreshed.setThing(standardTemplate == null ? oldSnapshot.getThing() : standardTemplate.getThing());
            refreshed.setRecordValues(templateRecordValuesMap.get(templateId));
            refreshed.setDeviceDefaultsInitialized(oldSnapshot == null ? 1
                    : oldSnapshot.getDeviceDefaultsInitialized());
            refreshed.setTemperature(templateTemperatureMap.get(templateId));
            refreshed.setHumidity(templateHumidityMap.get(templateId));
            // åˆ·æ–°æ¨¡æ¿ç»“构时必须保留检验阶段填写的业务数据,避免进入复核后回显为空。
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderServiceImpl.java
@@ -581,6 +581,9 @@
                        .thenComparing(item -> item.getTemplateRowIndex() == null
                                ? Integer.MAX_VALUE : item.getTemplateRowIndex()))
                .collect(Collectors.toList());
        if (templateItems.isEmpty()) {
            throw new ErrorException("所选原始记录模板未配置检验项或导出值,不能下单");
        }
        StandardMethod standardMethod = standardMethodService.getById(standard.getStandardMethodId());
        if (standardMethod == null || StringUtils.isBlank(standardMethod.getCode())) {
@@ -596,6 +599,7 @@
        order.setIsExemption(0);
        order.setFactory(null);
        order.setLaboratory(standard.getLaboratory());
        order.setStandardTreeId(dto.getStandardTreeId());
        order.setSampleType(null);
        order.setSample(standardCode);
        order.setPrepareUserEn(null);
@@ -740,6 +744,9 @@
                .sorted(Comparator.comparing((TemplateInspectionItemDto item) -> templateSortMap.get(item.getTemplateId()))
                        .thenComparing(item -> item.getTemplateRowIndex() == null ? Integer.MAX_VALUE : item.getTemplateRowIndex()))
                .collect(Collectors.toList());
        if (templateItems.isEmpty()) {
            throw new ErrorException("所选原始记录模板未配置检验项或导出值,不能下单");
        }
        StandardMethod standardMethod = standardMethodService.getById(standard.getStandardMethodId());
        if (standardMethod == null || StringUtils.isBlank(standardMethod.getCode())) {
            throw new ErrorException("标准编号不存在");
@@ -754,6 +761,7 @@
        order.setIsExemption(currentOrder.getIsExemption());
        order.setFactory(null);
        order.setLaboratory(standard.getLaboratory());
        order.setStandardTreeId(dto.getStandardTreeId());
        order.setSampleType(null);
        order.setSample(standardCode);
        order.setPrepareUserEn(null);
@@ -839,6 +847,7 @@
            snapshot.setRemark(template.getRemark());
            snapshot.setThing(template.getThing());
            snapshot.setSort(selection.getSort());
            snapshot.setDeviceDefaultsInitialized(0);
            insOrderStandardTemplateMapper.insert(snapshot);
            StandardTreeTemplateBindingVo binding = bindingMap.get(selection.getTemplateId());
@@ -929,7 +938,9 @@
                        .eq(StandardMethod::getCode, insOrder.getSample())
                        .select(StandardMethod::getId)).stream()
                .map(StandardMethod::getId).collect(Collectors.toList());
        if (!methodIds.isEmpty() && !orderTemplateIds.isEmpty()) {
        if (insOrder.getStandardTreeId() != null) {
            map.put("standardTreeId", insOrder.getStandardTreeId());
        } else if (!methodIds.isEmpty() && !orderTemplateIds.isEmpty()) {
            List<StandardTree> candidates = standardTreeService.list(Wrappers.<StandardTree>lambdaQuery()
                    .in(StandardTree::getStandardMethodId, methodIds)
                    .isNull(StandardTree::getSampleType)
@@ -1053,8 +1064,10 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> getInsOrderAndSample(Integer id, String laboratory) {
        Map<String, Object> map = new HashMap<>();
        initializeTemplateDevicesOnFirstOpen(id);
//        InsOrder insOrder = insOrderMapper.selectById(id);
        IfsOrderVO ifsOrderVO = insOrderMapper.selectOrderInfoById(id);
        Map<Integer, List<Integer>> templateDeviceIds = insOrderTemplateDeviceMapper.selectList(
@@ -1097,6 +1110,127 @@
        return map;
    }
    /** é¦–次打开检验任务时,将仍为空的模板设备从对应标准补入订单。 */
    private void initializeTemplateDevicesOnFirstOpen(Integer orderId) {
        InsOrder order = insOrderMapper.selectById(orderId);
        if (order == null || !DIRECT_TEMPLATE_LABORATORY.equals(order.getLaboratory())
                || !Objects.equals(order.getTypeSource(), -1)
                || !Objects.equals(order.getState(), 1)
                || "QUALITY_MONITOR".equals(order.getTaskSource())) {
            return;
        }
        List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateMapper.selectList(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                        .orderByAsc(InsOrderStandardTemplate::getSort, InsOrderStandardTemplate::getId)
                        .last("FOR UPDATE"));
        if (snapshots.isEmpty() || snapshots.stream().allMatch(
                snapshot -> Objects.equals(snapshot.getDeviceDefaultsInitialized(), 1))) {
            return;
        }
        Set<Integer> existingTemplateIds = insOrderTemplateDeviceMapper.selectList(
                Wrappers.<InsOrderTemplateDevice>lambdaQuery()
                        .eq(InsOrderTemplateDevice::getInsOrderId, orderId))
                .stream().map(InsOrderTemplateDevice::getTemplateId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        boolean inspectionStarted = order.getFirstInspectDate() != null
                || (order.getInsState() != null && order.getInsState() > 0)
                || snapshots.stream().anyMatch(snapshot -> StringUtils.isNotBlank(snapshot.getRecordValues())
                        || snapshot.getDetectionTime() != null || snapshot.getTemperature() != null
                        || snapshot.getDetectionPlace() != null);
        Map<Integer, List<Integer>> defaults = inspectionStarted ? Collections.emptyMap()
                : findStandardDeviceDefaults(order, snapshots);
        for (InsOrderStandardTemplate snapshot : snapshots) {
            if (Objects.equals(snapshot.getDeviceDefaultsInitialized(), 1)) {
                continue;
            }
            Integer templateId = snapshot.getTemplateId();
            if (!inspectionStarted && !existingTemplateIds.contains(templateId)
                    && defaults.containsKey(templateId)) {
                List<Integer> deviceIds = defaults.get(templateId);
                for (int index = 0; index < deviceIds.size(); index++) {
                    InsOrderTemplateDevice relation = new InsOrderTemplateDevice();
                    relation.setInsOrderId(orderId);
                    relation.setTemplateId(templateId);
                    relation.setDeviceId(deviceIds.get(index));
                    relation.setSort(index);
                    insOrderTemplateDeviceMapper.insert(relation);
                }
            }
            // æ— æ³•唯一确定来源时也只尝试这一次,避免以后标准库调整时意外改动订单。
            insOrderStandardTemplateMapper.update(null,
                    Wrappers.<InsOrderStandardTemplate>lambdaUpdate()
                            .eq(InsOrderStandardTemplate::getId, snapshot.getId())
                            .set(InsOrderStandardTemplate::getDeviceDefaultsInitialized, 1));
        }
    }
    private Map<Integer, List<Integer>> findStandardDeviceDefaults(InsOrder order,
            List<InsOrderStandardTemplate> snapshots) {
        Set<Integer> templateIds = snapshots.stream().map(InsOrderStandardTemplate::getTemplateId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        Integer standardTreeId = order.getStandardTreeId();
        if (standardTreeId == null && StringUtils.isNotBlank(order.getSample()) && !templateIds.isEmpty()) {
            List<Integer> methodIds = standardMethodService.list(Wrappers.<StandardMethod>lambdaQuery()
                            .eq(StandardMethod::getCode, order.getSample())
                            .select(StandardMethod::getId)).stream()
                    .map(StandardMethod::getId).collect(Collectors.toList());
            if (!methodIds.isEmpty()) {
                List<StandardTree> candidates = standardTreeService.list(Wrappers.<StandardTree>lambdaQuery()
                        .in(StandardTree::getStandardMethodId, methodIds)
                        .eq(StandardTree::getLaboratory, order.getLaboratory())
                        .isNotNull(StandardTree::getSample)
                        .isNull(StandardTree::getSampleType)
                        .isNull(StandardTree::getModel));
                List<StandardTree> matches = candidates.stream()
                        .filter(tree -> StringUtils.isNotBlank(tree.getSample()))
                        .filter(tree -> {
                            Set<Integer> boundIds = standardTreeService.selectTemplateBindings(tree.getId()).stream()
                                    .map(StandardTreeTemplateBindingVo::getTemplateId).collect(Collectors.toSet());
                            return boundIds.containsAll(templateIds);
                        }).collect(Collectors.toList());
                if (matches.size() == 1) {
                    standardTreeId = matches.get(0).getId();
                } else {
                    List<StandardTree> exact = matches.stream().filter(tree ->
                            standardTreeService.selectTemplateBindings(tree.getId()).stream()
                                    .map(StandardTreeTemplateBindingVo::getTemplateId)
                                    .collect(Collectors.toSet()).equals(templateIds))
                            .collect(Collectors.toList());
                    if (exact.size() == 1) {
                        standardTreeId = exact.get(0).getId();
                    }
                }
            }
        }
        if (standardTreeId == null) {
            return Collections.emptyMap();
        }
        StandardTree standard = standardTreeService.getById(standardTreeId);
        if (standard == null || StringUtils.isBlank(standard.getSample())
                || !Objects.equals(standard.getLaboratory(), order.getLaboratory())
                || standard.getSampleType() != null || standard.getModel() != null) {
            return Collections.emptyMap();
        }
        if (order.getStandardTreeId() == null) {
            insOrderMapper.update(null, Wrappers.<InsOrder>lambdaUpdate()
                    .eq(InsOrder::getId, order.getId())
                    .isNull(InsOrder::getStandardTreeId)
                    .set(InsOrder::getStandardTreeId, standardTreeId));
        }
        List<StandardTreeTemplateBindingVo> bindings = standardTreeService.selectTemplateBindings(standardTreeId);
        Map<Integer, List<Integer>> defaults = new HashMap<>();
        for (StandardTreeTemplateBindingVo binding : bindings) {
            if (templateIds.contains(binding.getTemplateId())) {
                defaults.put(binding.getTemplateId(), binding.getDeviceIds() == null
                        ? Collections.emptyList() : binding.getDeviceIds());
            }
        }
        return defaults;
    }
    @Override
    public IPage<SampleProductDto2> selectSampleAndProductByOrderId(IPage<SampleProductDto2> page, SampleProductDto2 sampleProductDto) {
        IPage<SampleProductDto2> productDto2IPage = insOrderMapper.selectSampleAndProductByOrderId(page,
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsProductServiceImpl.java
@@ -94,6 +94,11 @@
                                        InsOrderStandardTemplate::getInsResult, dto.getInsResult()));
            }
            if (dto.isDeviceIdsPresent()) {
                // æ‰‹åŠ¨é€‰æ‹©æˆ–æ¸…ç©ºéƒ½è§†ä¸ºå·²åˆå§‹åŒ–ï¼ŒåŽç»­è¿›å…¥ä»»åŠ¡ä¸å¾—å†æ¬¡å¥—ç”¨æ ‡å‡†åº“é»˜è®¤å€¼ã€‚
                insOrderStandardTemplateMapper.update(null,
                        Wrappers.<InsOrderStandardTemplate>lambdaUpdate()
                                .eq(InsOrderStandardTemplate::getId, snapshot.getId())
                                .set(InsOrderStandardTemplate::getDeviceDefaultsInitialized, 1));
                insOrderTemplateDeviceMapper.delete(
                        Wrappers.<InsOrderTemplateDevice>lambdaQuery()
                                .eq(InsOrderTemplateDevice::getInsOrderId, dto.getId())
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsSampleReceiptServiceImpl.java
@@ -4,46 +4,44 @@
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.basic.dto.TemplateInspectionItemDto;
import com.ruoyi.basic.utils.StandardTemplateInspectionItemParser;
import com.ruoyi.common.utils.GZipUtil;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.inspect.dto.SampleReceiptActionDto;
import com.ruoyi.inspect.dto.SampleReceiptPageDto;
import com.ruoyi.inspect.dto.SampleReceiptQueryDto;
import com.ruoyi.inspect.mapper.InsOrderMapper;
import com.ruoyi.inspect.mapper.InsOrderStandardTemplateMapper;
import com.ruoyi.inspect.mapper.InsProductMapper;
import com.ruoyi.inspect.mapper.InsSampleMapper;
import com.ruoyi.inspect.mapper.InsSampleReceiptMapper;
import com.ruoyi.inspect.mapper.InsSampleUserMapper;
import com.ruoyi.inspect.pojo.InsOrder;
import com.ruoyi.inspect.pojo.InsOrderStandardTemplate;
import com.ruoyi.inspect.pojo.InsProduct;
import com.ruoyi.inspect.pojo.InsSample;
import com.ruoyi.inspect.pojo.InsSampleReceipt;
import com.ruoyi.inspect.pojo.InsSampleUser;
import com.ruoyi.inspect.mapper.*;
import com.ruoyi.inspect.pojo.*;
import com.ruoyi.inspect.service.InsOrderService;
import com.ruoyi.inspect.service.InsOrderStateService;
import com.ruoyi.inspect.service.InsSampleReceiptService;
import com.ruoyi.system.mapper.UserMapper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
@AllArgsConstructor
public class InsSampleReceiptServiceImpl extends ServiceImpl<InsSampleReceiptMapper, InsSampleReceipt>
        implements InsSampleReceiptService {
    private static final String DIRECT_TEMPLATE_LABORATORY = "新聚";
    private final InsSampleReceiptMapper receiptMapper;
    private final InsOrderMapper insOrderMapper;
    private final InsOrderStandardTemplateMapper insOrderStandardTemplateMapper;
@@ -55,13 +53,17 @@
    private final UserMapper userMapper;
    @Override
    public com.baomidou.mybatisplus.core.metadata.IPage<SampleReceiptPageDto> page(Page<?> page, SampleReceiptQueryDto query) {
        return receiptMapper.selectPage(page, query);
    public IPage<SampleReceiptPageDto> page(Page<?> page, SampleReceiptQueryDto query) {
        IPage<SampleReceiptPageDto> result = receiptMapper.selectPage(page, query);
        result.getRecords().forEach(this::fillInspectionItemsFromSnapshots);
        return result;
    }
    @Override
    public SampleReceiptPageDto detail(Integer id) {
        return receiptMapper.selectDetail(id);
        SampleReceiptPageDto detail = receiptMapper.selectDetail(id);
        fillInspectionItemsFromSnapshots(detail);
        return detail;
    }
    @Override
@@ -91,7 +93,26 @@
        Long productCount = insProductMapper.selectCount(Wrappers.<InsProduct>lambdaQuery()
                .eq(InsProduct::getInsSampleId, sample.getId()).eq(InsProduct::getState, 1));
        if (productCount == 0) {
            throw new ErrorException("该委托未绑定检验项目,不能提交检验");
            List<TemplateInspectionItemDto> items = getSnapshotInspectionItems(order.getId());
            if (items.isEmpty()) {
                throw new ErrorException("订单原始记录模板未配置检验项或导出值,不能提交检验");
            }
            int sort = 0;
            for (TemplateInspectionItemDto item : items) {
                InsProduct product = new InsProduct();
                product.setInsSampleId(sample.getId());
                product.setInspectionItem(item.getInspectionItem());
                product.setInspectionItemClass("");
                product.setInspectionItemSubclass("");
                product.setLaboratory(order.getLaboratory());
                product.setSample(order.getSample());
                product.setTemplateId(item.getTemplateId());
                product.setTemplateRowIndex(item.getTemplateRowIndex());
                product.setSonLaboratory(DIRECT_TEMPLATE_LABORATORY);
                product.setState(1);
                product.setSort(sort++);
                insProductMapper.insert(product);
            }
        }
        if (userMapper.selectById(dto.getMainInspectorId()) == null) {
            throw new ErrorException("主检验人不存在或已停用");
@@ -160,9 +181,43 @@
        }
    }
    private List<TemplateInspectionItemDto> getSnapshotInspectionItems(Integer orderId) {
        List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateMapper.selectList(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                        .orderByAsc(InsOrderStandardTemplate::getSort, InsOrderStandardTemplate::getId));
        List<TemplateInspectionItemDto> items = new ArrayList<>();
        for (InsOrderStandardTemplate snapshot : snapshots) {
            String thing = snapshot.getThing();
            try {
                thing = GZipUtil.uncompress(thing);
            } catch (RuntimeException ignored) {
                // å…¼å®¹æœªåŽ‹ç¼©çš„åŽ†å²æ¨¡æ¿å¿«ç…§ã€‚
            }
            items.addAll(StandardTemplateInspectionItemParser.parse(
                    snapshot.getTemplateId(), snapshot.getSort(), thing));
        }
        return items;
    }
    private void fillInspectionItemsFromSnapshots(SampleReceiptPageDto row) {
        if (row == null || row.getOrderId() == null || StringUtils.isNotBlank(row.getInspectionItem())) {
            return;
        }
        try {
            String names = getSnapshotInspectionItems(row.getOrderId()).stream()
                    .map(TemplateInspectionItemDto::getInspectionItem)
                    .distinct().collect(Collectors.joining("\n"));
            row.setInspectionItem(names);
        } catch (RuntimeException ignored) {
            // åˆ—表仍可查看;提交检验时会返回具体的模板解析错误。
        }
    }
    @Override
    public void export(SampleReceiptQueryDto query, HttpServletResponse response) {
        List<SampleReceiptPageDto> list = receiptMapper.selectExportList(query);
        list.forEach(this::fillInspectionItemsFromSnapshots);
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("UTF-8");
        try {