package cn.iocoder.yudao.module.mes.service.dv.metering; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringImportExcelVO; import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringImportRespVO; import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.infra.api.config.ConfigApi; import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringPageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO; import cn.iocoder.yudao.module.mes.dal.mysql.dv.metering.MesDvMeteringMapper; import cn.iocoder.yudao.module.mes.service.dv.meteringcheck.MesDvMeteringCheckService; import cn.hutool.core.util.StrUtil; import jakarta.annotation.Resource; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.time.LocalDate; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Collection; import java.util.Collections; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_METERING_CODE_DUPLICATE; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_METERING_HAS_CHECK_RECORD; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_METERING_NOT_EXISTS; /** * MES 计量器具 Service 实现类 * * @author 超级管理员 */ @Service @Validated public class MesDvMeteringServiceImpl implements MesDvMeteringService { /** * 提醒天数配置 KEY(infra_config) */ private static final String REMIND_DAYS_CONFIG_KEY = "mes.dv-metering.remind-days"; /** * 提醒天数默认值 */ private static final int DEFAULT_REMIND_DAYS = 30; @Resource private MesDvMeteringMapper meteringMapper; @Resource @Lazy // 延迟加载,避免与计量检测记录 Service 循环依赖 private MesDvMeteringCheckService meteringCheckService; @Resource private MesDvMeteringRemindService meteringRemindService; @Resource private ConfigApi configApi; @Resource private DeptApi deptApi; @Resource private AdminUserApi adminUserApi; @Override public Long createMetering(MesDvMeteringSaveReqVO createReqVO) { // 校验编码唯一 validateMeteringCodeUnique(null, createReqVO.getCode()); // 插入 MesDvMeteringDO metering = BeanUtils.toBean(createReqVO, MesDvMeteringDO.class); meteringMapper.insert(metering); return metering.getId(); } @Override @Transactional(rollbackFor = Exception.class) public MesDvMeteringImportRespVO importMeteringList(List importList) { MesDvMeteringImportRespVO respVO = MesDvMeteringImportRespVO.builder() .createList(new ArrayList<>()) .updateList(new ArrayList<>()) .failureList(new LinkedHashMap<>()) .build(); if (CollUtil.isEmpty(importList)) { return respVO; } // 1. 预热“部门名称 -> 部门编号”映射(部门/责任人为非必填,匹配不到该行失败) Map deptIdMap = new HashMap<>(); List depts = deptApi.getDeptList(); if (CollUtil.isNotEmpty(depts)) { depts.forEach(dept -> { if (StrUtil.isNotBlank(dept.getName())) { deptIdMap.putIfAbsent(dept.getName().trim(), dept.getId()); } }); } // 2. 逐行导入(单行失败不影响其它行) for (int i = 0; i < importList.size(); i++) { MesDvMeteringImportExcelVO row = importList.get(i); if (row == null || (StrUtil.isBlank(row.getCode()) && StrUtil.isBlank(row.getName()))) { continue; // 跳过空行 } String label = StrUtil.isNotBlank(row.getCode()) ? row.getCode().trim() : "第 " + (i + 2) + " 行"; try { boolean restored = createMeteringByImport(row, deptIdMap); if (restored) { respVO.getUpdateList().add(row.getCode().trim()); // 恢复/覆盖更新 } else { respVO.getCreateList().add(row.getCode().trim()); } } catch (Exception ex) { respVO.getFailureList().put(label, resolveImportErrorMessage(ex)); } } return respVO; } /** * 依据导入行创建一条计量器具记录 */ private boolean createMeteringByImport(MesDvMeteringImportExcelVO row, Map deptIdMap) { // 1. 必填校验 if (StrUtil.isBlank(row.getCode())) { throw new IllegalArgumentException("计量器具编码不能为空"); } if (StrUtil.isBlank(row.getName())) { throw new IllegalArgumentException("计量器具名称不能为空"); } if (row.getCategory() == null) { throw new IllegalArgumentException("器具类别不能为空(请填字典中文,如 卡尺/其他)"); } if (row.getStatus() == null) { throw new IllegalArgumentException("状态不能为空(请填字典中文,如 在用/停用/报废)"); } // 2. 编码判重(含“已被逻辑删除”的记录):未删除 => 报错;已删除 => 稍后恢复并更新 MesDvMeteringDO exist = meteringMapper.selectByCodeIncludeDeleted(row.getCode().trim()); if (exist != null && !Boolean.TRUE.equals(exist.getDeleted())) { throw new IllegalArgumentException("计量器具编码已存在:" + row.getCode()); } // 3. 组装 MesDvMeteringSaveReqVO reqVO = new MesDvMeteringSaveReqVO(); reqVO.setCode(row.getCode().trim()); reqVO.setName(row.getName().trim()); reqVO.setCategory(row.getCategory()); reqVO.setSpecification(row.getSpecification()); reqVO.setBrand(row.getBrand()); reqVO.setPrecision(row.getPrecision()); reqVO.setRange(row.getRange()); reqVO.setCheckCycleCount(row.getCheckCycleCount() == null ? 12 : row.getCheckCycleCount()); reqVO.setUseDate(parseImportUseDate(row.getUseDate())); reqVO.setStatus(row.getStatus()); reqVO.setRemark(row.getRemark()); // 使用部门:按名称匹配 if (StrUtil.isNotBlank(row.getDeptName())) { Long deptId = deptIdMap.get(row.getDeptName().trim()); if (deptId == null) { throw new IllegalArgumentException("使用部门不存在:" + row.getDeptName()); } reqVO.setDeptId(deptId); } // 责任人:按昵称匹配 if (StrUtil.isNotBlank(row.getChargeUserName())) { Long userId = matchUserIdByNickname(row.getChargeUserName().trim()); if (userId == null) { throw new IllegalArgumentException("责任人不存在:" + row.getChargeUserName()); } reqVO.setChargeUserId(userId); } // 4. 落库:命中“已删除”的同编码 => 先恢复再更新;否则新增 if (exist != null) { meteringMapper.restoreDeletedById(exist.getId()); reqVO.setId(exist.getId()); updateMetering(reqVO); return true; // 表示“恢复/更新” } createMetering(reqVO); return false; } /** * 按昵称匹配用户编号(精确匹配优先,取不到则用模糊结果第一条) */ private Long matchUserIdByNickname(String nickname) { List users = adminUserApi.getUserListByNickname(nickname); if (CollUtil.isEmpty(users)) { return null; } return users.stream() .filter(user -> nickname.equals(user.getNickname())) .map(AdminUserRespDTO::getId) .findFirst() .orElseGet(() -> users.get(0).getId()); } /** * 解析投入使用日期(yyyy-MM-dd / yyyy/M/d);为空返回 null */ private LocalDate parseImportUseDate(String text) { if (StrUtil.isBlank(text)) { return null; } String value = text.trim(); for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) { try { return LocalDate.parse(value, java.time.format.DateTimeFormatter.ofPattern(pattern)); } catch (Exception ignored) { // 继续尝试下一个格式 } } try { return java.time.LocalDateTime.parse(value, java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")).toLocalDate(); } catch (Exception ignored) { // 忽略 } throw new IllegalArgumentException("投入使用日期格式不正确:" + text + ",示例 2026-01-01"); } /** * 提取导入失败原因(ServiceException 取业务提示) */ private String resolveImportErrorMessage(Exception ex) { if (ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException serviceException) { return serviceException.getMessage(); } return StrUtil.blankToDefault(ex.getMessage(), "导入失败"); } @Override public void updateMetering(MesDvMeteringSaveReqVO updateReqVO) { // 校验存在 validateMeteringExists(updateReqVO.getId()); // 校验编码唯一 validateMeteringCodeUnique(updateReqVO.getId(), updateReqVO.getCode()); // 更新 MesDvMeteringDO updateObj = BeanUtils.toBean(updateReqVO, MesDvMeteringDO.class); meteringMapper.updateById(updateObj); } @Override public void deleteMetering(Long id) { // 校验存在 validateMeteringExists(id); // 校验无关联检测记录 if (meteringCheckService.getMeteringCheckCountByMeteringId(id) > 0) { throw exception(DV_METERING_HAS_CHECK_RECORD); } // 删除 meteringMapper.deleteById(id); } @Override public void validateMeteringExists(Long id) { if (meteringMapper.selectById(id) == null) { throw exception(DV_METERING_NOT_EXISTS); } } private void validateMeteringCodeUnique(Long id, String code) { if (StrUtil.isBlank(code)) { return; } MesDvMeteringDO metering = meteringMapper.selectByCode(code); if (metering == null) { return; } if (ObjUtil.notEqual(metering.getId(), id)) { throw exception(DV_METERING_CODE_DUPLICATE); } } @Override public MesDvMeteringDO getMetering(Long id) { return meteringMapper.selectById(id); } @Override public PageResult getMeteringPage(MesDvMeteringPageReqVO pageReqVO) { // 1. 到期状态转换为日期范围,走 DB 过滤 Integer expireStatus = pageReqVO.getExpireStatus(); if (expireStatus != null) { LocalDate today = LocalDate.now(); int remindDays = getRemindDays(); switch (expireStatus) { case 1 -> { // 已过期:到期日 < 今天 pageReqVO.setNextCheckDateEnd(today.minusDays(1)); } case 2 -> { // 即将到期:到期日在 [今天, 今天+提醒天数] pageReqVO.setNextCheckDateBegin(today); pageReqVO.setNextCheckDateEnd(today.plusDays(remindDays)); } case 3 -> { // 正常:到期日 > 今天+提醒天数 pageReqVO.setNextCheckDateBegin(today.plusDays(remindDays + 1)); } default -> { } } } // 2. 查询 PageResult pageResult = meteringMapper.selectPage(pageReqVO); // 3. 懒触发到期提醒站内信(best-effort,不影响查询) meteringRemindService.sendExpireRemind(); return pageResult; } @Override public int getRemindDays() { String value = configApi.getConfigValueByKey(REMIND_DAYS_CONFIG_KEY); if (StrUtil.isBlank(value)) { return DEFAULT_REMIND_DAYS; } try { return Integer.parseInt(value.trim()); } catch (NumberFormatException ex) { return DEFAULT_REMIND_DAYS; } } @Override public void updateMeteringCheckInfo(Long meteringId, LocalDate lastCheckDate, LocalDate nextCheckDate) { meteringMapper.updateById(new MesDvMeteringDO() .setId(meteringId) .setLastCheckDate(lastCheckDate) .setNextCheckDate(nextCheckDate)); } @Override public List getMeteringList() { return meteringMapper.selectList(); } @Override public List getMeteringList(Collection ids) { if (CollUtil.isEmpty(ids)) { return Collections.emptyList(); } return meteringMapper.selectByIds(ids); } }