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