cnas-device/src/main/java/com/ruoyi/device/controller/DeviceExaminePlanController.java
@@ -168,17 +168,27 @@ @Operation(summary = "修改设备核查计划详情") @PostMapping("/updateDeviceExaminePlanDetail") public Result updateDeviceExaminePlanDetail(@RequestBody DeviceExaminePlanDetails deviceExaminePlanDetail){ return Result.success(deviceExaminePlanDetailsService.updateById(deviceExaminePlanDetail)); return Result.success(deviceExaminePlanService.updateExaminePlanDetail(deviceExaminePlanDetail)); } /** * 删除设备核查计划详情 * 删除设备核查计划详情(已有核查记录的不允许删除) * @return */ @Operation(summary = "删除设备核查计划详情") @DeleteMapping("/delDeviceExaminePlanDetail") public Result delDeviceExaminePlanDetail(Integer planDetailsId){ return Result.success(deviceExaminePlanDetailsService.removeById(planDetailsId)); return Result.success(deviceExaminePlanService.removeExaminePlanDetail(planDetailsId)); } /** * 为指定设备找一条可录入核查记录的计划明细,没有则新增本轮明细 * @return */ @Operation(summary = "按设备获取本轮核查计划明细") @GetMapping("/resolveExaminePlanDetail") public Result<DeviceExaminePlanDetails> resolveExaminePlanDetail(Integer deviceId, String checkTime){ return Result.success(deviceExaminePlanService.resolveExaminePlanDetail(deviceId, checkTime)); } /** cnas-device/src/main/java/com/ruoyi/device/pojo/DeviceExaminePlanDetails.java
@@ -57,6 +57,12 @@ @Schema(description = "备注") private String remark; @Schema(description = "附件名称") private String fileName; @Schema(description = "附件路径") private String fileUrl; @Schema(description = "创建人") @TableField(fill = FieldFill.INSERT) private Integer createUser; cnas-device/src/main/java/com/ruoyi/device/service/DeviceExaminePlanService.java
@@ -96,14 +96,36 @@ DeviceExaminePlanDto getOrCreatePlanByYear(String planYear); /** * 设备档案变更后,把设备同步到对应年份的设备核查计划明细(仅当已进入提醒期/已过期时生成) * 设备档案变更后,立即把设备登记到下次核查日期所在年份的设备核查计划明细 * @param device 设备 */ void syncDeviceExamineDetail(Device device); /** * 核查记录复核通过后,滚动设备档案的最近/下次核查日期 * 核查记录复核通过后,滚动设备档案的最近/下次核查日期,并登记下一轮计划 * @param planDetailsId 计划明细id */ void updateDeviceExamineDate(Integer planDetailsId); /** * 为指定设备找一条可录入核查记录的计划明细,没有则新增本轮明细 * @param deviceId 设备id * @param checkTime 核查月份(yyyy-MM),决定明细所属的计划年份 * @return 计划明细 */ DeviceExaminePlanDetails resolveExaminePlanDetail(Integer deviceId, String checkTime); /** * 修改计划明细(核查月份跨年时归到对应年份的计划) * @param detail 计划明细 * @return 是否修改成功 */ boolean updateExaminePlanDetail(DeviceExaminePlanDetails detail); /** * 删除计划明细(已有核查记录的不允许删除) * @param planDetailsId 计划明细id * @return 是否删除成功 */ boolean removeExaminePlanDetail(Integer planDetailsId); } cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceExaminePlanServiceImpl.java
@@ -22,10 +22,14 @@ import com.ruoyi.device.excel.upload.DeviceExaminePlanUpload; import com.ruoyi.device.mapper.DeviceExaminePlanDetailsMapper; import com.ruoyi.device.mapper.DeviceExaminePlanMapper; import com.ruoyi.device.mapper.DeviceExamineRecordContrastMapper; import com.ruoyi.device.mapper.DeviceExamineRecordMapper; import com.ruoyi.device.mapper.DeviceMapper; import com.ruoyi.device.pojo.Device; import com.ruoyi.device.pojo.DeviceExaminePlan; import com.ruoyi.device.pojo.DeviceExaminePlanDetails; import com.ruoyi.device.pojo.DeviceExamineRecord; import com.ruoyi.device.pojo.DeviceExamineRecordContrast; import com.ruoyi.device.service.DeviceExaminePlanDetailsService; import com.ruoyi.device.service.DeviceExaminePlanService; import com.ruoyi.framework.exception.ErrorException; @@ -49,6 +53,7 @@ import java.time.LocalDateTime; import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -77,6 +82,10 @@ private DeviceMapper deviceMapper; @Resource private UserMapper userMapper; @Resource private DeviceExamineRecordMapper deviceExamineRecordMapper; @Resource private DeviceExamineRecordContrastMapper deviceExamineRecordContrastMapper; /** * 新增设备核查计划 @@ -381,15 +390,15 @@ } /** * 设备档案变更后,把设备同步到对应年份的设备核查计划明细 * 设备档案变更后,立即把设备登记到下次核查日期所在年份的设备核查计划明细 */ @Override public void syncDeviceExamineDetail(Device device) { LocalDate remind = calcExamineRemindDate(device); if (remind == null || remind.isAfter(LocalDate.now())) { LocalDate due = calcExamineDueDate(device); if (due == null) { return; } DeviceExaminePlan plan = getOrCreatePlanEntity(String.valueOf(remind.getYear())); DeviceExaminePlan plan = getOrCreatePlanEntity(String.valueOf(due.getYear())); fillExamineDetailIfAbsent(plan, device); } @@ -406,6 +415,7 @@ if (device == null) { return; } // 提前核查时计划月份可能还没到,取实际完成当天;否则按本轮计划核查月份 LocalDate checkDate = parseCheckDate(detail.getCheckTime()); if (checkDate.isAfter(LocalDate.now())) { checkDate = LocalDate.now(); @@ -423,6 +433,8 @@ } } deviceMapper.updateById(device); // 本轮核查完成后立即登记下一轮,历史明细和记录保持不动 syncDeviceExamineDetail(device); } /** @@ -445,7 +457,7 @@ } /** * 扫描所有设备,把已到点(下次核查日期前1个月起,含已过期)的设备补进本年度计划 * 扫描所有设备,把本年度及往年逾期未核查的设备补进计划 */ private void ensureExamineDetails(DeviceExaminePlan plan) { List<Device> devices = deviceMapper.selectList(null); @@ -455,9 +467,9 @@ } /** * 设备提醒起点:下次核查日期前1个月(没填下次日期时用最近日期+核查周期兜底) * 设备本轮应核查日期(没填下次日期时用最近日期+核查周期兜底) */ private LocalDate calcExamineRemindDate(Device device) { private LocalDate calcExamineDueDate(Device device) { LocalDateTime base = device.getNextExamineDate(); if (base == null && device.getLastExamineDate() != null && StringUtils.isNotBlank(device.getExamineCycle())) { try { @@ -465,21 +477,21 @@ } catch (NumberFormatException ignored) { } } return base == null ? null : base.toLocalDate().minusMonths(1); return base == null ? null : base.toLocalDate(); } /** * 已到点且计划中尚无该设备本轮明细时自动补一条 * 计划中尚无该设备本轮明细时自动补一条;核查时间取应核查月份 */ private void fillExamineDetailIfAbsent(DeviceExaminePlan plan, Device device) { LocalDate remind = calcExamineRemindDate(device); if (remind == null || remind.isAfter(LocalDate.now())) { LocalDate due = calcExamineDueDate(device); if (due == null) { return; } if (plan.getPlanYear() != null && String.valueOf(remind.getYear()).compareTo(plan.getPlanYear()) > 0) { if (plan.getPlanYear() != null && String.valueOf(due.getYear()).compareTo(plan.getPlanYear()) > 0) { return; } String checkTime = remind.format(DateTimeFormatter.ofPattern("yyyy-MM")); String checkTime = due.format(DateTimeFormatter.ofPattern("yyyy-MM")); Long exist = deviceExaminePlanDetailsMapper.selectCount(Wrappers.<DeviceExaminePlanDetails>lambdaQuery() .eq(DeviceExaminePlanDetails::getPlanId, plan.getPlanId()) .eq(DeviceExaminePlanDetails::getDeviceId, device.getId()) @@ -515,4 +527,122 @@ } return LocalDate.now(); } /** * 为指定设备找一条可录入核查记录的计划明细;没有可用明细时在当前年度计划新增本轮明细 */ @Override public DeviceExaminePlanDetails resolveExaminePlanDetail(Integer deviceId, String checkTime) { if (deviceId == null) { throw new ErrorException("请选择核查设备"); } Device device = deviceMapper.selectById(deviceId); if (device == null) { throw new ErrorException("核查设备不存在"); } String checkMonth = normalizeCheckMonth(checkTime); DeviceExaminePlan plan = getOrCreatePlanEntity(checkMonth.substring(0, 4)); List<DeviceExaminePlanDetails> details = deviceExaminePlanDetailsMapper.selectList( Wrappers.<DeviceExaminePlanDetails>lambdaQuery() .eq(DeviceExaminePlanDetails::getDeviceId, deviceId) .orderByAsc(DeviceExaminePlanDetails::getCheckTime)); DeviceExaminePlanDetails nearest = null; long nearestDistance = Long.MAX_VALUE; for (DeviceExaminePlanDetails detail : details) { // 已有核查记录的明细代表已完成的一轮,后续核查永远不复用、不覆盖 Long recordCount = deviceExamineRecordMapper.selectCount(Wrappers.<DeviceExamineRecord>lambdaQuery() .eq(DeviceExamineRecord::getPlanDetailsId, detail.getPlanDetailsId())); if (recordCount != null && recordCount > 0) { continue; } if (detail.getPlanId() != null && detail.getPlanId().equals(plan.getPlanId())) { return detail; } long distance = monthDistance(detail.getCheckTime(), checkMonth); if (distance < nearestDistance) { nearest = detail; nearestDistance = distance; } } // 提前核查时优先复用系统登记、尚未核查的这一轮(可能在别的年份计划里) if (nearest != null) { return nearest; } DeviceExaminePlanDetails detail = new DeviceExaminePlanDetails(); detail.setPlanId(plan.getPlanId()); detail.setDeviceId(device.getId()); String deviceName = device.getDeviceName() == null ? "" : device.getDeviceName(); if (StringUtils.isNotBlank(device.getSpecificationModel())) { deviceName = deviceName + " " + device.getSpecificationModel(); } detail.setDeviceName(deviceName); detail.setDeviceNumber(device.getManagementNumber()); detail.setCheckTime(checkMonth); deviceExaminePlanDetailsMapper.insert(detail); return detail; } /** * 规范核查月份为 yyyy-MM,非法或为空时取当前月份 */ private String normalizeCheckMonth(String checkTime) { if (StringUtils.isNotBlank(checkTime)) { String normalized = checkTime.trim().replace('.', '-').replace('/', '-'); if (normalized.length() >= 7) { try { return YearMonth.parse(normalized.substring(0, 7)).toString(); } catch (Exception ignored) { } } } return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")); } /** * 两个月份之间相差的月数 */ private long monthDistance(String fromCheckTime, String targetMonth) { try { YearMonth from = YearMonth.parse(normalizeCheckMonth(fromCheckTime)); YearMonth target = YearMonth.parse(targetMonth); return Math.abs(ChronoUnit.MONTHS.between(from, target)); } catch (Exception e) { return Long.MAX_VALUE; } } /** * 修改计划明细;核查月份跨年时,把明细归到核查月份所属年份的计划下 */ @Override public boolean updateExaminePlanDetail(DeviceExaminePlanDetails detail) { if (detail == null || detail.getPlanDetailsId() == null) { throw new ErrorException("缺少设备核查计划明细id"); } if (StringUtils.isNotBlank(detail.getCheckTime())) { String checkMonth = normalizeCheckMonth(detail.getCheckTime()); detail.setCheckTime(checkMonth); DeviceExaminePlan plan = getOrCreatePlanEntity(checkMonth.substring(0, 4)); detail.setPlanId(plan.getPlanId()); } return deviceExaminePlanDetailsMapper.updateById(detail) > 0; } /** * 删除计划明细;已有核查/比对记录的不允许删除,保证历史留档 */ @Override public boolean removeExaminePlanDetail(Integer planDetailsId) { if (planDetailsId == null) { throw new ErrorException("缺少设备核查计划明细id"); } Long recordCount = deviceExamineRecordMapper.selectCount(Wrappers.<DeviceExamineRecord>lambdaQuery() .eq(DeviceExamineRecord::getPlanDetailsId, planDetailsId)); Long contrastCount = deviceExamineRecordContrastMapper.selectCount(Wrappers.<DeviceExamineRecordContrast>lambdaQuery() .eq(DeviceExamineRecordContrast::getPlanDetailsId, planDetailsId)); if ((recordCount != null && recordCount > 0) || (contrastCount != null && contrastCount > 0)) { throw new ErrorException("该计划已存在核查记录,为保留历史记录不允许删除"); } return deviceExaminePlanDetailsService.removeById(planDetailsId); } } cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceServiceImpl.java
@@ -102,7 +102,13 @@ @Override public int addDeviceParameter(Device itemParameter) { return deviceMapper.insert(itemParameter); int rows = deviceMapper.insert(itemParameter); Device device = deviceMapper.selectById(itemParameter.getId()); if (device != null && (device.getNextExamineDate() != null || (device.getLastExamineDate() != null && StringUtils.isNotBlank(device.getExamineCycle())))) { deviceExaminePlanService.syncDeviceExamineDetail(device); } return rows; } @Override cnas-device/src/main/resources/mapper/DeviceExaminePlanDetailsMapper.xml
@@ -20,6 +20,8 @@ CASE WHEN d.check_charger_user='' or d.check_charger_user is null THEN u.name ELSE d.check_charger_user END AS check_charger_user, d.remark, d.file_name, d.file_url, d.create_user, d.create_time, d.update_user, @@ -43,11 +45,11 @@ left join device_examine_record dc on d.plan_details_id = dc.plan_details_id left join device_examine_record_contrast dd on dd.plan_details_id = d.plan_details_id left join device dev on dev.id = d.device_id order by check_time asc ) a <if test="ew.customSqlSegment != null and ew.customSqlSegment != ''"> ${ew.customSqlSegment} </if> order by device_number asc, check_time desc </select> </mapper> inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java
@@ -365,7 +365,7 @@ } } map.put("errorMsg", collect); map.put("missingExportItems", getMissingTemplateExportValues(orderId)); map.put("missingExportItems", getMissingTemplateExportValues(orderId, laboratory)); map.put("unInsOrderCount", 0L); return map; } @@ -649,10 +649,11 @@ JSONObject values = JSON.parseObject(JSON.toJSONString(dto.getValues() == null ? Collections.emptyMap() : dto.getValues())); // 检验期间页面展示的是标准维护中的最新模板,保存校验必须使用同一份模板, // 避免模板新增“导出值”批注后仍按下单时的旧快照误报未配置。 String currentTemplateThing = getCurrentTemplateThing(snapshot); List<String> exportValueCoordinates = getExportValueCoordinates(currentTemplateThing).values().stream() // 保存时必须使用页面当前展示的模板:待检、在检阶段展示标准维护中的实时模板, // 复核、驳回阶段展示订单快照;否则模板在检验途中被修改后会取错导出值单元格。 boolean useSnapshot = useSnapshotTemplate(dto.getOrderId(), getCurrentUserLaboratory(dto.getOrderId())); String pageTemplateThing = getPageTemplateThing(snapshot, useSnapshot); List<String> exportValueCoordinates = getExportValueCoordinates(pageTemplateThing).values().stream() .flatMap(Collection::stream) .distinct() .collect(Collectors.toList()); @@ -889,8 +890,8 @@ } /** * 检验保存、提交校验读取标准维护中的实时模板;模板已被删除或内容为空时才回退订单快照。 * 检验完成后的展示和报告仍由订单快照负责,不会随标准模板后续修改而变化。 * 读取标准维护中的实时模板;模板已被删除或内容为空时才回退订单快照。 * 仅用于检验室仍处于待检、在检阶段(页面展示实时模板)的保存与校验。 */ private String getCurrentTemplateThing(InsOrderStandardTemplate snapshot) { if (snapshot != null && snapshot.getTemplateId() != null) { @@ -903,9 +904,53 @@ } /** * 检验单页面在检验室进入复核及之后状态时展示订单快照,导出值的保存和提交校验必须与页面 * 使用同一份模板;否则标准模板在检验途中被修改后,历史单据会按新坐标读取快照数据而误报为空。 */ private String getPageTemplateThing(InsOrderStandardTemplate snapshot, boolean useSnapshot) { if (useSnapshot && snapshot != null && StrUtil.isNotBlank(snapshot.getThing())) { return snapshot.getThing(); } return getCurrentTemplateThing(snapshot); } /** * 与检验单页面 getTemplateThing 的判定保持一致:检验室状态进入复核(2)及之后时使用订单快照。 * 指派记录里的实验室名称可能和状态行不一致(如“全部”),此时退回按订单整体状态判定。 */ private boolean useSnapshotTemplate(Integer orderId, String laboratory) { if (StringUtils.isNotBlank(laboratory)) { InsOrderState state = insOrderStateMapper.selectOne(Wrappers.<InsOrderState>lambdaQuery() .eq(InsOrderState::getInsOrderId, orderId) .eq(InsOrderState::getLaboratory, laboratory) .last("limit 1")); if (state != null) { return state.getInsState() != null && state.getInsState() >= 2; } } return insOrderStateMapper.selectList(Wrappers.<InsOrderState>lambdaQuery() .eq(InsOrderState::getInsOrderId, orderId)) .stream() .anyMatch(state -> state.getInsState() != null && state.getInsState() >= 2); } /** * 取当前登录人在该订单上最近一条指派记录所属的检验室;未指派账号返回 null,由状态判定兜底。 */ private String getCurrentUserLaboratory(Integer orderId) { Integer userId = SecurityUtils.getUserId().intValue(); InsSampleUser sampleUser = insSampleUserMapper.selectOne(Wrappers.<InsSampleUser>lambdaQuery() .eq(InsSampleUser::getInsSampleId, orderId) .eq(InsSampleUser::getUserId, userId) .orderByDesc(InsSampleUser::getId) .last("limit 1")); return sampleUser == null ? null : sampleUser.getSonLaboratory(); } /** * 直绑模板订单只校验每个“导出值”批注所在区域右侧紧邻单元格,不使用检验项状态或 last_value 判定。 */ private List<String> getMissingTemplateExportValues(Integer orderId) { private List<String> getMissingTemplateExportValues(Integer orderId, String laboratory) { List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateService.list( Wrappers.<InsOrderStandardTemplate>lambdaQuery() .eq(InsOrderStandardTemplate::getInsOrderId, orderId) @@ -915,9 +960,10 @@ missing.add("当前订单未找到原始记录模板快照"); return missing; } boolean useSnapshot = useSnapshotTemplate(orderId, laboratory); for (InsOrderStandardTemplate snapshot : snapshots) { Integer templateId = snapshot.getTemplateId(); List<String> coordinates = getExportValueCoordinates(getCurrentTemplateThing(snapshot)).values().stream() List<String> coordinates = getExportValueCoordinates(getPageTemplateThing(snapshot, useSnapshot)).values().stream() .flatMap(Collection::stream) .distinct() .collect(Collectors.toList()); @@ -1273,7 +1319,7 @@ boolean directTemplateOrder = isDirectTemplateOrder(orderId); List<InsProduct> insProducts; if (directTemplateOrder) { List<String> missingExportValues = getMissingTemplateExportValues(orderId); List<String> missingExportValues = getMissingTemplateExportValues(orderId, laboratory); if (!missingExportValues.isEmpty()) { throw new ErrorException("模板中的导出值不能为空:\n" + String.join("\n", missingExportValues)); } @@ -1882,9 +1928,17 @@ } else { //复核不通过将把复核的负责人去掉 Integer id = insSampleUserMapper.selectOne(Wrappers.<InsSampleUser>lambdaQuery().eq(InsSampleUser::getInsSampleId, orderId).orderByDesc(InsSampleUser::getId).last("limit 1")).getId(); insSampleUserMapper.deleteById(id); // 复核不通过只去掉本检验室本次提交的复核负责人;按复核人状态和检验室定位, // 避免误删其他检验室的记录或检验人员记录。 InsSampleUser reviewerRow = insSampleUserMapper.selectOne(Wrappers.<InsSampleUser>lambdaQuery() .eq(InsSampleUser::getInsSampleId, orderId) .eq(InsSampleUser::getState, 1) .eq(InsSampleUser::getSonLaboratory, laboratory) .orderByDesc(InsSampleUser::getId) .last("limit 1")); if (reviewerRow != null) { insSampleUserMapper.deleteById(reviewerRow.getId()); } // 当前审核人 Integer currentUserId = SecurityUtils.getUserId().intValue(); ruoyi-system/src/main/java/com/ruoyi/system/mapper/UserMapper.java
@@ -26,6 +26,13 @@ List<User> selectUserCondition(@Param("ew") QueryWrapper<User> ew, @Param("type") String type); /** * 根据条件获取用户列表(包含超级管理员) * @param ew * @return */ List<User> selectUserConditionWithAdmin(@Param("ew") QueryWrapper<User> ew); /** * 获取用户部门 * @param userId * @return ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UserServiceImpl.java
@@ -43,6 +43,8 @@ return baseMapper.selectDepartmentLimsUserList(SecurityUtils.getUserId().intValue()); case "3": return baseMapper.selectInspectorUserList(); case "4": return baseMapper.selectUserConditionWithAdmin(QueryWrappers.queryWrappers(user)); } } ruoyi-system/src/main/resources/mapper/system/UserMapper.xml
@@ -31,6 +31,34 @@ </if> </select> <!--根据条件获取用户列表(包含超级管理员)--> <select id="selectUserConditionWithAdmin" resultType="com.ruoyi.common.core.domain.entity.User"> select * from (select id, dept_id, account, name, name_en, user_type, email, phone, sex, age, signature_url, picture_url, status, del_flag, login_ip, login_date, depart_lims_id, company, is_custom from user where del_flag = '0') a <if test="ew.customSqlSegment != null and ew.customSqlSegment != ''"> ${ew.customSqlSegment} </if> </select> <!-- 获取用户部门 --> <select id="selectUserDepartmentLimsName" resultType="java.lang.String"> select dl.name