cnas-process/src/main/java/com/ruoyi/process/service/impl/QualitySuperviseServiceImpl.java
@@ -20,7 +20,6 @@
import com.ruoyi.common.constant.MenuJumpPathConstants;
import com.ruoyi.common.core.domain.entity.InformationNotification;
import com.ruoyi.common.core.domain.entity.User;
import com.ruoyi.common.utils.DateImageUtil;
import com.ruoyi.common.utils.QueryWrappers;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.WxCpUtils;
@@ -57,7 +56,9 @@
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -214,6 +215,92 @@
    }
    /**
     * 监督计划下发
     * @param qualitySupervise
     * @return
     */
    @Override
    public boolean issueQualitySupervise(QualitySupervise qualitySupervise) {
        if (qualitySupervise.getSuperviseId() == null) {
            throw new ErrorException("缺少监督计划id");
        }
        QualitySupervise supervise = baseMapper.selectById(qualitySupervise.getSuperviseId());
        if (supervise == null) {
            throw new ErrorException("未找到监督计划");
        }
        if (!Integer.valueOf(1).equals(supervise.getRatifyStatus())) {
            throw new ErrorException("计划未批准,不能下发");
        }
        if (Integer.valueOf(1).equals(supervise.getIssueStatus())) {
            throw new ErrorException("计划已下发,不能重复下发");
        }
        baseMapper.update(null, Wrappers.<QualitySupervise>lambdaUpdate()
                .eq(QualitySupervise::getSuperviseId, supervise.getSuperviseId())
                .set(QualitySupervise::getIssueStatus, 1)
                .set(QualitySupervise::getIssueTime, LocalDateTime.now())
        );
        // 当前登录用户
        Integer userId = SecurityUtils.getUserId().intValue();
        User user = userMapper.selectById(userId);
        // 接收人: 计划监督员 + 所有明细的被监督人(去重)
        LinkedHashSet<Integer> receiveUserIds = new LinkedHashSet<>();
        if (StringUtils.isNotBlank(supervise.getRecordUserIds())) {
            for (String idStr : supervise.getRecordUserIds().split(",")) {
                if (StringUtils.isNotBlank(idStr)) {
                    receiveUserIds.add(Integer.valueOf(idStr.trim()));
                }
            }
        }
        List<QualitySuperviseDetails> detailsList = qualitySuperviseDetailsMapper.selectList(Wrappers.<QualitySuperviseDetails>lambdaQuery()
                .eq(QualitySuperviseDetails::getSuperviseId, supervise.getSuperviseId()));
        for (QualitySuperviseDetails detail : detailsList) {
            if (detail.getSupervisedUserId() != null) {
                receiveUserIds.add(detail.getSupervisedUserId());
            }
        }
        // 消息发送
        for (Integer receiveUserId : receiveUserIds) {
            InformationNotification info = new InformationNotification();
            // 发送人
            info.setCreateUser(user.getName());
            info.setMessageType("6");
            info.setTheme("CNAS质量监督计划下发通知");
            info.setContent(supervise.getSuperviseName() + " 质量监督计划已下发,请按计划开展质量监督,并及时填写日常监督检查记录");
            info.setSenderId(userId);
            // 接收人
            info.setConsigneeId(receiveUserId);
            info.setJumpPath(MenuJumpPathConstants.QUALITY_SUPERVISE);
            informationNotificationService.addInformationNotification(info);
        }
        // 发送企业微信通知
        threadPoolTaskExecutor.execute(() -> {
            String message = "";
            message += "CNAS质量监督计划下发通知";
            message += "\n请去过程要求-质量监督计划";
            message += "\n" + supervise.getSuperviseName() + " 质量监督计划已下发,请按计划开展质量监督,并及时填写日常监督检查记录";
            for (Integer receiveUserId : receiveUserIds) {
                User receiveUser = userMapper.selectById(receiveUserId);
                if (receiveUser == null) {
                    continue;
                }
                //发送企业微信消息通知
                try {
                    WxCpUtils.inform(receiveUser.getAccount(), message, null);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        });
        return true;
    }
    /**
     * 监督计划列表
     * @param page
     * @param qualitySupervise
@@ -283,10 +370,8 @@
            for (String userIdStr : userIds) {
                // 转换为int类型
                Integer userId = Integer.valueOf(userIdStr);
                // 获取人员签名对象
                PictureRenderData finalUserSignatureUrl = UserUtils.getFinalUserSignatureUrl(userId);
                // 将人员签名对象添加到集合中
                recordUserDataList.add(finalUserSignatureUrl);
                // 获取人员签名对象(缺失时兜底透明图, 避免导出失败)
                recordUserDataList.add(signatureOrTransparent(userId));
            }
        }
@@ -302,6 +387,8 @@
        int index = 1;
        for (QualitySuperviseDetails qualitySuperviseDetail : qualitySuperviseDetails) {
            qualitySuperviseDetail.setIndex(index);
            qualitySuperviseDetail.setSuperviseFrequency(buildSuperviseFrequency(qualitySuperviseDetail.getSuperviseTime()));
            qualitySuperviseDetail.setSuperviseTime(toMonthText(qualitySuperviseDetail.getSuperviseTime()));
            index++;
        }
@@ -315,14 +402,14 @@
                new HashMap<String, Object>() {{
                    put("year", qualitySupervise.getSuperviseYear());
                    put("superviseDetailList", qualitySuperviseDetails);
                    put("writeUrl", StringUtils.isNotBlank(writeUrl) ? Pictures.ofLocal(imgUrl + "/" + writeUrl).create() : null);
                    put("ratifyUrl", StringUtils.isNotBlank(finalRatifyUrl) ? Pictures.ofLocal(imgUrl + "/" + finalRatifyUrl).create() : null);
                    put("writeDateUrl", qualitySupervise.getWriteTime() != null ?
                            Pictures.ofStream(DateImageUtil.createDateImage(qualitySupervise.getWriteTime())).create() : null);
                    put("ratifyDateUrl", qualitySupervise.getRatifyTime() != null ?
                            Pictures.ofStream(DateImageUtil.createDateImage(qualitySupervise.getRatifyTime())).create() : null);
                    put("recordUserUrl1", recordUserDataList.get(0));
                    put("recordUserUrl2", recordUserDataList.get(1));
                    put("writeUrl", orTransparentPicture(StringUtils.isNotBlank(writeUrl) ? Pictures.ofLocal(imgUrl + "/" + writeUrl).create() : null));
                    put("ratifyUrl", orTransparentPicture(StringUtils.isNotBlank(finalRatifyUrl) ? Pictures.ofLocal(imgUrl + "/" + finalRatifyUrl).create() : null));
                    put("writeDateStr", qualitySupervise.getWriteTime() != null ?
                            qualitySupervise.getWriteTime().format(SUPERVISE_DATE_FORMATTER) : "");
                    put("ratifyDateStr", qualitySupervise.getRatifyTime() != null ?
                            qualitySupervise.getRatifyTime().format(SUPERVISE_DATE_FORMATTER) : "");
                    put("recordUserUrl1", orTransparentPicture(recordUserDataList.get(0)));
                    put("recordUserUrl2", orTransparentPicture(recordUserDataList.get(1)));
                }});
        try {
@@ -342,6 +429,49 @@
        }
    }
    /**
     * 监督频次: 按监督时间中的月份个数计算, 如 "2026.3" -> "1次/年", "3月、12月" -> "2次/年"
     */
    private static String buildSuperviseFrequency(String superviseTime) {
        if (StringUtils.isBlank(superviseTime)) {
            return "";
        }
        int count = 0;
        for (String part : superviseTime.split("[、,,;;/\\s]+")) {
            if (part.matches(".*\\d.*")) {
                count++;
            }
        }
        return count > 0 ? count + "次/年" : "";
    }
    /**
     * 监督时间: 表格中只展示月份, 如 "2026.3" -> "3月", "3月、12月" -> "3月、12月"
     */
    private static String toMonthText(String superviseTime) {
        if (StringUtils.isBlank(superviseTime)) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        for (String part : superviseTime.split("[、,,;;]+")) {
            String s = part.trim();
            if (s.isEmpty()) {
                continue;
            }
            int idx = s.lastIndexOf('.');
            String month = (idx >= 0 ? s.substring(idx + 1) : s).replaceAll("\\D", "");
            if (month.isEmpty() || month.length() > 2) {
                return superviseTime;
            }
            month = month.replaceFirst("^0+(?=\\d)", "");
            if (sb.length() > 0) {
                sb.append("、");
            }
            sb.append(month).append("月");
        }
        return sb.length() > 0 ? sb.toString() : superviseTime;
    }
    /************************************************ 记录 ******************************************************/
@@ -408,13 +538,9 @@
            detailsRecord.setSuperviseDetailsId(superviseDetailsId);
            detailsRecord.setTestMember(qualitySuperviseDetails.getSupervisee());
            detailsRecord.setSupervisor(supervisor);
            detailsRecord.setPersonnel(SecurityUtils.getLoginUser().getUser().getNickName() + "有相应检测员的上岗证");
            detailsRecord.setEnvironment("温度:()℃ 湿度:()%");
            detailsRecord.setInspectionRecord("检测人员" + SecurityUtils.getLoginUser().getUser().getNickName() + "进行检测记录,记录内容真实有效");
            detailsRecord.setExaminingReport("由" + SecurityUtils.getLoginUser().getUser().getNickName() + "出示的检测报告符合规范要求");
            detailsRecord.setSupervisionEvaluation("检测按照要求进行,判定为满意");
            detailsRecord.setHandlingAdvice("/");
            // 默认监督检测日期为当天
            detailsRecord.setTestDate(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.M.d")));
            detailsRecord.setProblemDesc("无");
        }
        // 添加批准人名称
@@ -517,7 +643,7 @@
    }
    /**
     * 导出监督记录表
     * 导出监督记录表(日常监督检查记录表)
     * @param superviseDetailsId
     * @param response
     */
@@ -525,33 +651,26 @@
    public void exportSuperviseDetailRecord(Integer superviseDetailsId, HttpServletResponse response) {
        QualitySuperviseDetailsRecord recordDto = qualitySuperviseDetailsRecordMapper.selectSuperviseDetailRecord(superviseDetailsId);
        // 查询检测人员
        User tserUser = new User();
        if (StringUtils.isNotBlank(recordDto.getTestMember())) {
            tserUser = userMapper.selectOne(Wrappers.<User>lambdaQuery()
                    .eq(User::getName, recordDto.getTestMember())
                    .last("limit 1"));
        }
        // 监督内容勾选展示串(照纸张表排布)
        recordDto.setSuperviseContentStr(buildSuperviseContentStr(recordDto.getSuperviseContent()));
        // 获取路径
        InputStream inputStream = this.getClass().getResourceAsStream("/static/supervision-detail-record.docx");
        InputStream inputStream = this.getClass().getResourceAsStream("/static/daily-supervise-record.docx");
        ConfigureBuilder builder = Configure.builder();
        builder.useSpringEL(true);
        User finalTserUser = tserUser;
        XWPFTemplate template = XWPFTemplate.compile(inputStream, builder.build()).render(
                new HashMap<String, Object>() {{
                    put("supervision", recordDto);
                    put("testMemberUrl", UserUtils.getFinalUserSignatureUrl(finalTserUser.getId()));
                    put("supervisoruUrl", UserUtils.getFinalUserSignatureUrl(recordDto.getSupervisor()));
                    put("technicalDirectorUrl", UserUtils.getFinalUserSignatureUrl(recordDto.getRatifyUserId()));
                    put("technicalDirectorDateUrl", recordDto.getRatifyTime() != null ?
                            Pictures.ofStream(DateImageUtil.createDateImage(recordDto.getRatifyTime())).create() : null);
                    put("supervisorUrl", signatureOrTransparent(recordDto.getSupervisor()));
                    put("qualityUrl", signatureOrTransparent(recordDto.getRatifyUserId()));
                    put("qualityDateStr", recordDto.getRatifyTime() != null ?
                            recordDto.getRatifyTime().format(RECORD_DATE_FORMATTER) : "");
                }});
        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();
@@ -562,6 +681,85 @@
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("导出失败");
        }
    }
    /**
     * 监督内容(9项, 按纸张表横排顺序)
     */
    private static final String[] SUPERVISE_CONTENT_ITEMS = {
            "人员持证及培训", "设施及环境条件", "检测方法",
            "仪器设备管理", "测量溯源性", "标准物质管理",
            "样品制备和处置", "检测数据处理", "结果报告结果"
    };
    /**
     * 拼接"监督内容"勾选展示串, 每3项一行
     */
    private String buildSuperviseContentStr(String superviseContent) {
        String content = StringUtils.isNotBlank(superviseContent) ? superviseContent : "";
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < SUPERVISE_CONTENT_ITEMS.length; i++) {
            String item = SUPERVISE_CONTENT_ITEMS[i];
            sb.append(content.contains(item) ? "☑" : "□").append(item).append("  ");
            if (i % 3 == 2) {
                sb.append("\n");
            }
        }
        return sb.toString().trim();
    }
    /**
     * 全透明 1x1 PNG(已验证像素 A=0, 拉伸到原标签尺寸后不可见), 图片缺失时兜底
     */
    private static final byte[] TRANSPARENT_PNG = Base64.getDecoder().decode(
            "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAANSURBVBhXY2BgYGAAAAAFAAGKM+MAAAAAAElFTkSuQmCC");
    /**
     * 图片标签值兜底: null 或本地图片读取失败(如签名文件缺失)时返回透明图片
     */
    private PictureRenderData orTransparentPicture(PictureRenderData picture) {
        if (picture != null) {
            try {
                byte[] data = picture.readPictureData();
                if (data != null && data.length > 0) {
                    return picture;
                }
            } catch (Exception ignored) {
            }
        }
        return Pictures.ofBytes(TRANSPARENT_PNG).create();
    }
    /**
     * 导出日期文字格式(如 2026年1月9日)
     */
    private static final DateTimeFormatter SUPERVISE_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy年M月d日");
    /**
     * 记录表日期文字格式(与表内"检测日期"字段一致, 如 2026.10.14)
     */
    private static final DateTimeFormatter RECORD_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy.M.d");
    /**
     * 用户签名兜底(按ID): 人员不存在或签名未配置时接口会直接抛错, 此处返回透明图片
     */
    private PictureRenderData signatureOrTransparent(Integer userId) {
        try {
            return orTransparentPicture(UserUtils.getFinalUserSignatureUrl(userId));
        } catch (Exception e) {
            return Pictures.ofBytes(TRANSPARENT_PNG).create();
        }
    }
    /**
     * 用户签名兜底(按姓名): 人员不存在或签名未配置时接口会直接抛错, 此处返回透明图片
     */
    private PictureRenderData signatureOrTransparent(String userName) {
        try {
            return orTransparentPicture(UserUtils.getFinalUserSignatureUrl(userName));
        } catch (Exception e) {
            return Pictures.ofBytes(TRANSPARENT_PNG).create();
        }
    }
@@ -861,10 +1059,10 @@
        XWPFTemplate template = XWPFTemplate.compile(inputStream, builder.build()).render(
                new HashMap<String, Object>() {{
                    put("control", exportDto);
                    put("discovererUrl", StringUtils.isNotBlank(finalDiscovererUrl) ? Pictures.ofLocal(imgUrl + "/" + finalDiscovererUrl).create() : null);
                    put("responsibleUrl", StringUtils.isNotBlank(finalResponsibleUrl) ? Pictures.ofLocal(imgUrl + "/" + finalResponsibleUrl).create() : null);
                    put("correctiveUrl", StringUtils.isNotBlank(finalCorrectiveUrl) ? Pictures.ofLocal(imgUrl + "/" + finalCorrectiveUrl).create() : null);
                    put("qualityUrl", StringUtils.isNotBlank(finalQualityUrl) ? Pictures.ofLocal(imgUrl + "/" + finalQualityUrl).create() : null);
                    put("discovererUrl", orTransparentPicture(StringUtils.isNotBlank(finalDiscovererUrl) ? Pictures.ofLocal(imgUrl + "/" + finalDiscovererUrl).create() : null));
                    put("responsibleUrl", orTransparentPicture(StringUtils.isNotBlank(finalResponsibleUrl) ? Pictures.ofLocal(imgUrl + "/" + finalResponsibleUrl).create() : null));
                    put("correctiveUrl", orTransparentPicture(StringUtils.isNotBlank(finalCorrectiveUrl) ? Pictures.ofLocal(imgUrl + "/" + finalCorrectiveUrl).create() : null));
                    put("qualityUrl", orTransparentPicture(StringUtils.isNotBlank(finalQualityUrl) ? Pictures.ofLocal(imgUrl + "/" + finalQualityUrl).create() : null));
                }});
        try {
@@ -1240,10 +1438,10 @@
        XWPFTemplate template = XWPFTemplate.compile(inputStream, builder.build()).render(
                new HashMap<String, Object>() {{
                    put("correct", detailsCorrectDto);
                    put("raiseUrl", StringUtils.isNotBlank(finalRaiseUrl) ? Pictures.ofLocal(imgUrl + "/" + finalRaiseUrl).create() : null);
                    put("causeUrl", StringUtils.isNotBlank(finalCauseUrl) ? Pictures.ofLocal(imgUrl + "/" + finalCauseUrl).create() : null);
                    put("correctUrl", StringUtils.isNotBlank(finalCorrectUrl) ? Pictures.ofLocal(imgUrl + "/" + finalCorrectUrl).create() : null);
                    put("validationUrl", StringUtils.isNotBlank(finalValidationUrl) ? Pictures.ofLocal(imgUrl + "/" + finalValidationUrl).create() : null);
                    put("raiseUrl", orTransparentPicture(StringUtils.isNotBlank(finalRaiseUrl) ? Pictures.ofLocal(imgUrl + "/" + finalRaiseUrl).create() : null));
                    put("causeUrl", orTransparentPicture(StringUtils.isNotBlank(finalCauseUrl) ? Pictures.ofLocal(imgUrl + "/" + finalCauseUrl).create() : null));
                    put("correctUrl", orTransparentPicture(StringUtils.isNotBlank(finalCorrectUrl) ? Pictures.ofLocal(imgUrl + "/" + finalCorrectUrl).create() : null));
                    put("validationUrl", orTransparentPicture(StringUtils.isNotBlank(finalValidationUrl) ? Pictures.ofLocal(imgUrl + "/" + finalValidationUrl).create() : null));
                }});
        try {