package com.ruoyi.device.service.impl;
|
|
import com.alibaba.excel.EasyExcel;
|
import com.alibaba.excel.context.AnalysisContext;
|
import com.alibaba.excel.event.AnalysisEventListener;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
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;
|
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.QueryWrappers;
|
import com.ruoyi.common.utils.SecurityUtils;
|
import com.ruoyi.common.utils.WxCpUtils;
|
import com.ruoyi.device.dto.DeviceExaminePlanDetailsDto;
|
import com.ruoyi.device.dto.DeviceExaminePlanDto;
|
import com.ruoyi.device.excel.upload.DeviceExaminePlanUpload;
|
import com.ruoyi.device.mapper.DeviceExamineAttachmentMapper;
|
import com.ruoyi.device.mapper.DeviceExaminePlanDetailsMapper;
|
import com.ruoyi.device.mapper.DeviceExaminePlanMapper;
|
import com.ruoyi.device.mapper.DeviceExamineRecordContrastDetailsMapper;
|
import com.ruoyi.device.mapper.DeviceExamineRecordContrastMapper;
|
import com.ruoyi.device.mapper.DeviceExamineRecordDetailMapper;
|
import com.ruoyi.device.mapper.DeviceExamineRecordMapper;
|
import com.ruoyi.device.mapper.DeviceMapper;
|
import com.ruoyi.device.pojo.Device;
|
import com.ruoyi.device.pojo.DeviceExamineAttachment;
|
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.pojo.DeviceExamineRecordContrastDetails;
|
import com.ruoyi.device.pojo.DeviceExamineRecordDetail;
|
import com.ruoyi.device.service.DeviceExaminePlanDetailsService;
|
import com.ruoyi.device.service.DeviceExaminePlanService;
|
import com.ruoyi.framework.exception.ErrorException;
|
import com.ruoyi.system.mapper.UserMapper;
|
import com.ruoyi.system.service.InformationNotificationService;
|
import org.apache.poi.ss.usermodel.BorderStyle;
|
import org.apache.poi.ss.usermodel.Cell;
|
import org.apache.poi.ss.usermodel.CellStyle;
|
import org.apache.poi.ss.usermodel.Font;
|
import org.apache.poi.ss.usermodel.HorizontalAlignment;
|
import org.apache.poi.ss.usermodel.Row;
|
import org.apache.poi.ss.usermodel.Sheet;
|
import org.apache.poi.ss.usermodel.VerticalAlignment;
|
import org.apache.poi.ss.usermodel.Workbook;
|
import org.apache.poi.ss.util.CellRangeAddress;
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.web.multipart.MultipartFile;
|
|
import jakarta.annotation.Resource;
|
import jakarta.servlet.http.HttpServletResponse;
|
import java.io.File;
|
import java.io.IOException;
|
import java.io.OutputStream;
|
import java.net.URLEncoder;
|
import java.time.LocalDate;
|
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;
|
import java.util.Map;
|
import java.util.stream.Collectors;
|
|
/**
|
* <p>
|
* 设备核查计划主表 服务实现类
|
* </p>
|
*
|
* @author 江苏鵷雏网络科技有限公司
|
* @since 2024-12-16 07:14:04
|
*/
|
@Service
|
@Transactional(rollbackFor = Exception.class)
|
public class DeviceExaminePlanServiceImpl extends ServiceImpl<DeviceExaminePlanMapper, DeviceExaminePlan> implements DeviceExaminePlanService {
|
|
@Resource
|
private DeviceExaminePlanDetailsMapper deviceExaminePlanDetailsMapper;
|
@Resource
|
private DeviceExaminePlanDetailsService deviceExaminePlanDetailsService;
|
@Resource
|
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
@Resource
|
private InformationNotificationService informationNotificationService;
|
@Resource
|
private DeviceMapper deviceMapper;
|
@Resource
|
private UserMapper userMapper;
|
@Resource
|
private DeviceExamineRecordMapper deviceExamineRecordMapper;
|
@Resource
|
private DeviceExamineRecordContrastMapper deviceExamineRecordContrastMapper;
|
@Resource
|
private DeviceExamineRecordDetailMapper deviceExamineRecordDetailMapper;
|
@Resource
|
private DeviceExamineRecordContrastDetailsMapper deviceExamineRecordContrastDetailsMapper;
|
@Resource
|
private DeviceExamineAttachmentMapper deviceExamineAttachmentMapper;
|
|
@Value("${file.path}")
|
private String filePath;
|
|
@Value("${wordUrl}")
|
private String wordUrl;
|
|
/**
|
* 新增设备核查计划
|
* @param examinePlanDto
|
* @return
|
*/
|
@Override
|
public boolean addDeviceExaminePlan(DeviceExaminePlanDto examinePlanDto) {
|
// 当前登录用户
|
Integer userId = SecurityUtils.getUserId().intValue();
|
// 文件名称
|
examinePlanDto.setWriteUserId(userId);
|
examinePlanDto.setWriteTime(LocalDateTime.now());
|
baseMapper.insert(examinePlanDto);
|
|
// 添加详情
|
if (CollectionUtils.isNotEmpty(examinePlanDto.getExaminePlanDetailsList())) {
|
for (DeviceExaminePlanDetails deviceExaminePlanDetails : examinePlanDto.getExaminePlanDetailsList()) {
|
deviceExaminePlanDetails.setPlanId(examinePlanDto.getPlanId());
|
}
|
deviceExaminePlanDetailsService.saveBatch(examinePlanDto.getExaminePlanDetailsList());
|
}
|
return true;
|
}
|
|
/**
|
* 导入设备核查计划
|
* @param file
|
* @return
|
*/
|
@Override
|
public boolean importDeviceExaminePlan(MultipartFile file) {
|
// 当前登录用户
|
Integer userId = SecurityUtils.getUserId().intValue();
|
// 文件名称
|
String fileName = file.getOriginalFilename().substring(0, file.getOriginalFilename().lastIndexOf("."));
|
DeviceExaminePlan examinePlan = new DeviceExaminePlan();
|
examinePlan.setPlanName(fileName);
|
examinePlan.setWriteUserId(userId);
|
examinePlan.setWriteTime(LocalDateTime.now());
|
baseMapper.insert(examinePlan);
|
|
List<DeviceExaminePlanDetails> examinePlanDetails = new ArrayList<>();
|
// 导入附件内容
|
try {
|
// excel解析
|
EasyExcel.read(file.getInputStream(), DeviceExaminePlanUpload.class, new AnalysisEventListener<DeviceExaminePlanUpload>() {
|
@Override
|
public void invoke(DeviceExaminePlanUpload detailsUpload, AnalysisContext analysisContext) {
|
// 判断是否为空
|
if (StringUtils.isNotBlank(detailsUpload.getDeviceName()) &&
|
StringUtils.isNotBlank(detailsUpload.getDeviceNumber())) {
|
// 对象复制
|
DeviceExaminePlanDetails planDetails = new DeviceExaminePlanDetails();
|
BeanUtils.copyProperties(detailsUpload, planDetails);
|
|
planDetails.setPlanId(examinePlan.getPlanId());
|
examinePlanDetails.add(planDetails);
|
}
|
}
|
|
@Override
|
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
|
|
}
|
}).sheet().doRead();
|
for (DeviceExaminePlanDetails examinePlanDetail : examinePlanDetails) {
|
Device device = deviceMapper.selectOne(Wrappers.<Device>lambdaQuery()
|
.eq(Device::getManagementNumber, examinePlanDetail.getDeviceNumber().trim()));
|
if (device == null) {
|
throw new ErrorException("设备编号" + examinePlanDetail.getDeviceNumber() + "未查询到设备, 请重新导入");
|
}
|
User user = userMapper.selectOne(Wrappers.<User>lambdaQuery()
|
.eq(User::getName, examinePlanDetail.getCheckChargerUser()));
|
if (user == null) {
|
throw new ErrorException("设备编号" + examinePlanDetail.getDeviceNumber() + "未查询到核查人");
|
}
|
examinePlanDetail.setDeviceId(device.getId());
|
examinePlanDetail.setDeviceModel(device.getSpecificationModel());
|
examinePlanDetail.setCheckChargerUserId(user.getId());
|
}
|
|
deviceExaminePlanDetailsService.saveBatch(examinePlanDetails);
|
|
} catch (IOException e) {
|
throw new RuntimeException(e);
|
}
|
return true;
|
}
|
|
/**
|
* 提交批准
|
* @param deviceExaminePlan
|
* @return
|
*/
|
@Override
|
public boolean submitRatifyDeviceExaminePlan(DeviceExaminePlan deviceExaminePlan) {
|
baseMapper.update(null, Wrappers.<DeviceExaminePlan>lambdaUpdate()
|
.eq(DeviceExaminePlan::getPlanId, deviceExaminePlan.getPlanId())
|
.set(DeviceExaminePlan::getRatifyUserId, deviceExaminePlan.getRatifyUserId())
|
.set(DeviceExaminePlan::getRatifyRemark, null)
|
.set(DeviceExaminePlan::getRatifyStatus, null)
|
.set(DeviceExaminePlan::getRatifyTime, null)
|
);
|
|
DeviceExaminePlan examinePlan = baseMapper.selectById(deviceExaminePlan.getPlanId());
|
|
// 发送消息
|
Integer userId = SecurityUtils.getUserId().intValue();
|
User user = userMapper.selectById(userId);
|
// 消息发送
|
InformationNotification info = new InformationNotification();
|
// 发送人
|
info.setCreateUser(user.getName());
|
info.setMessageType("6");
|
info.setTheme("CNAS设备核查计划批准通知");
|
info.setContent(examinePlan.getPlanName() + "的设备核查计划待批准");
|
info.setSenderId(userId);
|
// 接收人
|
info.setConsigneeId(examinePlan.getRatifyUserId());
|
info.setJumpPath(MenuJumpPathConstants.DEVICE);
|
informationNotificationService.addInformationNotification(info);
|
|
// 发送企业微信通知
|
threadPoolTaskExecutor.execute(() -> {
|
// 查询接收人
|
User personnel = userMapper.selectById(examinePlan.getRatifyUserId());
|
|
String message = "";
|
message += "CNAS设备核查计划批准通知";
|
message += "\n请去资源管理-设备一层目录-设备核查计划填写";
|
message += "\n" + examinePlan.getPlanName() + "的校准核查划待批准";
|
//发送企业微信消息通知
|
try {
|
WxCpUtils.inform(personnel.getAccount(), message, null);
|
} catch (Exception e) {
|
throw new RuntimeException(e);
|
}
|
});
|
return false;
|
}
|
|
/**
|
* 设备核查计划批准
|
* @param deviceExaminePlan
|
* @return
|
*/
|
@Override
|
public boolean ratifyDeviceExaminePlan(DeviceExaminePlan deviceExaminePlan) {
|
// 当前登录用户
|
baseMapper.update(null, Wrappers.<DeviceExaminePlan>lambdaUpdate()
|
.eq(DeviceExaminePlan::getPlanId, deviceExaminePlan.getPlanId())
|
.set(DeviceExaminePlan::getRatifyRemark, deviceExaminePlan.getRatifyRemark())
|
.set(DeviceExaminePlan::getRatifyStatus, deviceExaminePlan.getRatifyStatus())
|
.set(DeviceExaminePlan::getRatifyTime, LocalDateTime.now())
|
);
|
return true;
|
}
|
|
/**
|
* 设备核查计划列表
|
* @param page
|
* @param deviceExaminePlan
|
* @return
|
*/
|
@Override
|
public IPage<DeviceExaminePlanDto> pageDeviceExaminePlan(Page page, DeviceExaminePlan deviceExaminePlan) {
|
return baseMapper.deviceExaminePlanDetailsMapper(page, QueryWrappers.queryWrappers(deviceExaminePlan));
|
}
|
|
/**
|
* 设备核查计划详情列表
|
* @param page
|
* @param deviceExaminePlanDetails
|
* @return
|
*/
|
@Override
|
public IPage<DeviceExaminePlanDetailsDto> pageDeviceExaminePlanDetail(Page page, DeviceExaminePlanDetails deviceExaminePlanDetails) {
|
if (deviceExaminePlanDetails.getPlanId() == null) {
|
return new Page();
|
}
|
return deviceExaminePlanDetailsMapper.pageDeviceExaminePlanDetail(page, QueryWrappers.queryWrappers(deviceExaminePlanDetails));
|
}
|
|
/**
|
* 导出设备核查计划(Excel):有筛选条件时导出筛选结果,没有筛选时导出该年份全部
|
* @param deviceExaminePlanId 设备核查计划id
|
* @param deviceName 设备名称筛选
|
* @param deviceNumber 设备编号筛选
|
* @param response 响应体
|
*/
|
@Override
|
public void exportDeviceExaminePlanDetail(Integer deviceExaminePlanId, String deviceName, String deviceNumber, HttpServletResponse response) {
|
DeviceExaminePlan deviceExaminePlan = baseMapper.selectById(deviceExaminePlanId);
|
if (deviceExaminePlan == null) {
|
throw new ErrorException("设备核查计划不存在");
|
}
|
DeviceExaminePlanDetails query = new DeviceExaminePlanDetails();
|
query.setPlanId(deviceExaminePlanId);
|
query.setDeviceName(deviceName);
|
query.setDeviceNumber(deviceNumber);
|
List<DeviceExaminePlanDetailsDto> deviceExaminePlanDetailsDtoList =
|
deviceExaminePlanDetailsMapper.listDeviceExaminePlanDetail(QueryWrappers.queryWrappers(query));
|
|
String planName = StringUtils.isNotBlank(deviceExaminePlan.getPlanName()) ? deviceExaminePlan.getPlanName() : "设备核查计划";
|
String[] columns = {"序号", "设备编号", "设备名称", "设备型号", "核查时间", "核查指标",
|
"核查方法", "结果如何判定", "核查负责人", "审核状态", "备注"};
|
List<String[]> dataRows = new ArrayList<>();
|
int index = 1;
|
for (DeviceExaminePlanDetailsDto detail : deviceExaminePlanDetailsDtoList) {
|
String checkTime = StringUtils.isNotBlank(detail.getCompleteDate()) ? detail.getCompleteDate() : detail.getCheckTime();
|
dataRows.add(new String[]{
|
String.valueOf(index++),
|
blankToEmpty(detail.getDeviceNumber()),
|
blankToEmpty(detail.getDeviceName()),
|
blankToEmpty(detail.getDeviceModel()),
|
blankToEmpty(checkTime),
|
blankToEmpty(detail.getCheckIndex()),
|
blankToEmpty(detail.getCheckMethod()),
|
blankToEmpty(detail.getHowResults()),
|
blankToEmpty(detail.getCheckChargerUser()),
|
recordStatusText(detail.getRecordStatus()),
|
blankToEmpty(detail.getRemark())
|
});
|
}
|
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
response.setCharacterEncoding("UTF-8");
|
try {
|
response.setHeader("Content-disposition",
|
"attachment;filename=" + URLEncoder.encode(planName, "UTF-8") + ".xlsx");
|
writePlanExcel(response, planName, columns, dataRows);
|
} catch (Exception e) {
|
e.printStackTrace();
|
throw new RuntimeException("导出失败");
|
}
|
}
|
|
/**
|
* 写计划表:标题行 + 列名 + 数据,整体只加最外边框,全部居中,无背景色
|
*/
|
private void writePlanExcel(HttpServletResponse response, String planName, String[] columns,
|
List<String[]> dataRows) throws IOException {
|
Workbook workbook = new XSSFWorkbook();
|
Map<String, CellStyle> styleCache = new HashMap<>();
|
Font titleFont = buildExcelFont(workbook, true, 16);
|
Font headerFont = buildExcelFont(workbook, true, 12);
|
Font contentFont = buildExcelFont(workbook, false, 12);
|
|
Sheet sheet = workbook.createSheet("设备核查计划");
|
int lastColumn = columns.length - 1;
|
|
Row titleRow = sheet.createRow(0);
|
titleRow.setHeightInPoints(30);
|
for (int c = 0; c <= lastColumn; c++) {
|
Cell cell = titleRow.createCell(c);
|
if (c == 0) {
|
cell.setCellValue(planName);
|
}
|
cell.setCellStyle(exportStyle(workbook, styleCache, titleFont, c == 0, c == lastColumn, true, false));
|
}
|
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, lastColumn));
|
|
Row headerRow = sheet.createRow(1);
|
headerRow.setHeightInPoints(22);
|
for (int c = 0; c <= lastColumn; c++) {
|
Cell cell = headerRow.createCell(c);
|
cell.setCellValue(columns[c]);
|
cell.setCellStyle(exportStyle(workbook, styleCache, headerFont,
|
c == 0, c == lastColumn, false, dataRows.isEmpty()));
|
}
|
|
for (int r = 0; r < dataRows.size(); r++) {
|
Row row = sheet.createRow(r + 2);
|
boolean lastDataRow = r == dataRows.size() - 1;
|
for (int c = 0; c <= lastColumn; c++) {
|
Cell cell = row.createCell(c);
|
cell.setCellValue(dataRows.get(r)[c]);
|
cell.setCellStyle(exportStyle(workbook, styleCache, contentFont,
|
c == 0, c == lastColumn, false, lastDataRow));
|
}
|
}
|
|
for (int c = 0; c <= lastColumn; c++) {
|
int width = textWidth(columns[c]);
|
for (String[] row : dataRows) {
|
width = Math.max(width, textWidth(row[c]));
|
}
|
sheet.setColumnWidth(c, Math.min(255, width + 4) * 256);
|
}
|
|
OutputStream os = response.getOutputStream();
|
workbook.write(os);
|
os.flush();
|
workbook.close();
|
}
|
|
private Font buildExcelFont(Workbook workbook, boolean bold, int size) {
|
Font font = workbook.createFont();
|
font.setFontName("宋体");
|
font.setBold(bold);
|
font.setFontHeightInPoints((short) size);
|
return font;
|
}
|
|
/**
|
* 单元格样式:居中、无背景色;内部细网格线,整体最外圈粗边框
|
*/
|
private CellStyle exportStyle(Workbook workbook, Map<String, CellStyle> styleCache, Font font,
|
boolean outerLeft, boolean outerRight, boolean outerTop, boolean outerBottom) {
|
String key = font.getBold() + "-" + font.getFontHeightInPoints() + "-" + (outerLeft ? 1 : 0)
|
+ (outerRight ? 1 : 0) + (outerTop ? 1 : 0) + (outerBottom ? 1 : 0);
|
CellStyle cached = styleCache.get(key);
|
if (cached != null) {
|
return cached;
|
}
|
CellStyle style = workbook.createCellStyle();
|
style.setFont(font);
|
style.setAlignment(HorizontalAlignment.CENTER);
|
style.setVerticalAlignment(VerticalAlignment.CENTER);
|
style.setBorderLeft(BorderStyle.THIN);
|
style.setBorderRight(BorderStyle.THIN);
|
style.setBorderTop(BorderStyle.THIN);
|
style.setBorderBottom(BorderStyle.THIN);
|
if (outerLeft) {
|
style.setBorderLeft(BorderStyle.MEDIUM);
|
}
|
if (outerRight) {
|
style.setBorderRight(BorderStyle.MEDIUM);
|
}
|
if (outerTop) {
|
style.setBorderTop(BorderStyle.MEDIUM);
|
}
|
if (outerBottom) {
|
style.setBorderBottom(BorderStyle.MEDIUM);
|
}
|
styleCache.put(key, style);
|
return style;
|
}
|
|
/**
|
* 估算显示宽度(中文按两个字符宽)
|
*/
|
private int textWidth(String text) {
|
if (text == null) {
|
return 0;
|
}
|
int width = 0;
|
for (int i = 0; i < text.length(); i++) {
|
width += text.charAt(i) > 0xFF ? 2 : 1;
|
}
|
return width;
|
}
|
|
/**
|
* 记录状态文案:0未开始、1待批准、2通过、3不通过
|
*/
|
private String recordStatusText(Integer recordStatus) {
|
if (recordStatus == null) {
|
return "未开始";
|
}
|
switch (recordStatus) {
|
case 1:
|
return "待批准";
|
case 2:
|
return "通过";
|
case 3:
|
return "不通过";
|
default:
|
return "未开始";
|
}
|
}
|
|
private String blankToEmpty(String value) {
|
return value == null ? "" : value;
|
}
|
|
@Override
|
public boolean updateDeviceExaminePlan(DeviceExaminePlanDto examinePlanDto) {
|
// 当前登录用户
|
Integer userId = SecurityUtils.getUserId().intValue();
|
// 文件名称
|
examinePlanDto.setWriteUserId(userId);
|
examinePlanDto.setWriteTime(LocalDateTime.now());
|
baseMapper.updateById(examinePlanDto);
|
|
deviceExaminePlanDetailsService.remove(Wrappers.<DeviceExaminePlanDetails>lambdaQuery()
|
.eq(DeviceExaminePlanDetails::getPlanId, examinePlanDto.getPlanId()));
|
|
// 添加详情
|
if (CollectionUtils.isNotEmpty(examinePlanDto.getExaminePlanDetailsList())) {
|
for (DeviceExaminePlanDetails deviceExaminePlanDetails : examinePlanDto.getExaminePlanDetailsList()) {
|
deviceExaminePlanDetails.setPlanId(examinePlanDto.getPlanId());
|
}
|
deviceExaminePlanDetailsService.saveBatch(examinePlanDto.getExaminePlanDetailsList());
|
}
|
return true;
|
}
|
|
@Override
|
public DeviceExaminePlanDto getDeviceExaminePlan(Integer planId) {
|
DeviceExaminePlan examinePlan = baseMapper.selectById(planId);
|
DeviceExaminePlanDto deviceExaminePlanDto = new DeviceExaminePlanDto();
|
BeanUtils.copyProperties(examinePlan, deviceExaminePlanDto);
|
// 查询详情
|
List<DeviceExaminePlanDetails> list = deviceExaminePlanDetailsService.list(Wrappers.<DeviceExaminePlanDetails>lambdaQuery()
|
.eq(DeviceExaminePlanDetails::getPlanId, planId)
|
.orderByAsc(DeviceExaminePlanDetails::getCheckTime));
|
deviceExaminePlanDto.setExaminePlanDetailsList(list);
|
|
return deviceExaminePlanDto;
|
}
|
|
/**
|
* 按年份查询设备核查计划(不存在则自动创建),并自动补全已到点的设备明细
|
*/
|
@Override
|
public DeviceExaminePlanDto getOrCreatePlanByYear(String planYear) {
|
if (StringUtils.isBlank(planYear)) {
|
planYear = String.valueOf(LocalDate.now().getYear());
|
}
|
DeviceExaminePlan plan = getOrCreatePlanEntity(planYear);
|
ensureExamineDetails(plan);
|
DeviceExaminePlanDto planDto = new DeviceExaminePlanDto();
|
BeanUtils.copyProperties(plan, planDto);
|
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
planDto.setWriteTimeStr(plan.getWriteTime() == null ? null : plan.getWriteTime().format(dateFormatter));
|
planDto.setRatifyTimeStr(plan.getRatifyTime() == null ? null : plan.getRatifyTime().format(dateFormatter));
|
if (plan.getWriteUserId() != null) {
|
User writeUser = userMapper.selectById(plan.getWriteUserId());
|
if (writeUser != null) {
|
planDto.setWriteName(writeUser.getName());
|
}
|
}
|
if (plan.getRatifyUserId() != null) {
|
User ratifyUser = userMapper.selectById(plan.getRatifyUserId());
|
if (ratifyUser != null) {
|
planDto.setRatifyName(ratifyUser.getName());
|
}
|
}
|
return planDto;
|
}
|
|
/**
|
* 核查计划年份列表:删除没有任何明细的空年份计划后,返回有明细的年份(倒序)
|
*/
|
@Override
|
public List<String> getExaminePlanYears() {
|
List<DeviceExaminePlan> plans = baseMapper.selectList(null);
|
for (DeviceExaminePlan plan : plans) {
|
Long detailCount = deviceExaminePlanDetailsMapper.selectCount(Wrappers.<DeviceExaminePlanDetails>lambdaQuery()
|
.eq(DeviceExaminePlanDetails::getPlanId, plan.getPlanId()));
|
if (detailCount == null || detailCount == 0) {
|
removeById(plan.getPlanId());
|
}
|
}
|
return baseMapper.selectPlanYears();
|
}
|
|
/**
|
* 设备档案变更后,立即把设备登记到下次核查日期所在年份的设备核查计划明细
|
*/
|
@Override
|
public void syncDeviceExamineDetail(Device device) {
|
LocalDate due = calcExamineDueDate(device);
|
if (due == null) {
|
return;
|
}
|
DeviceExaminePlan plan = getOrCreatePlanEntity(String.valueOf(due.getYear()));
|
fillExamineDetailIfAbsent(plan, device);
|
}
|
|
/**
|
* 复核通过后,把设备档案的最近/下次核查日期滚动到本轮
|
*/
|
@Override
|
public void updateDeviceExamineDate(Integer planDetailsId) {
|
DeviceExaminePlanDetails detail = deviceExaminePlanDetailsMapper.selectById(planDetailsId);
|
if (detail == null || detail.getDeviceId() == null) {
|
return;
|
}
|
Device device = deviceMapper.selectById(detail.getDeviceId());
|
if (device == null) {
|
return;
|
}
|
// 审核通过当天即视为实际核查完成时间
|
LocalDateTime checkDateTime = LocalDate.now().atStartOfDay();
|
// 只允许向前滚动,避免重复复核旧记录时把日期改回去
|
if (device.getLastExamineDate() != null && !checkDateTime.isAfter(device.getLastExamineDate())) {
|
return;
|
}
|
device.setLastExamineDate(checkDateTime);
|
if (StringUtils.isNotBlank(device.getExamineCycle())) {
|
try {
|
device.setNextExamineDate(checkDateTime.plusMonths(Long.parseLong(device.getExamineCycle().trim())));
|
} catch (NumberFormatException ignored) {
|
}
|
}
|
deviceMapper.updateById(device);
|
// 本轮核查完成后立即登记下一轮,历史明细和记录保持不动
|
syncDeviceExamineDetail(device);
|
}
|
|
/**
|
* 查询年份对应的计划,不存在则创建
|
*/
|
private DeviceExaminePlan getOrCreatePlanEntity(String planYear) {
|
DeviceExaminePlan plan = baseMapper.selectOne(Wrappers.<DeviceExaminePlan>lambdaQuery()
|
.eq(DeviceExaminePlan::getPlanYear, planYear)
|
.orderByDesc(DeviceExaminePlan::getPlanId)
|
.last("limit 1"));
|
if (plan == null) {
|
plan = new DeviceExaminePlan();
|
plan.setPlanName(planYear + "年度设备核查计划");
|
plan.setPlanYear(planYear);
|
plan.setWriteUserId(SecurityUtils.getUserId().intValue());
|
plan.setWriteTime(LocalDateTime.now());
|
baseMapper.insert(plan);
|
}
|
return plan;
|
}
|
|
/**
|
* 扫描所有设备,把本年度及往年逾期未核查的设备补进计划
|
*/
|
private void ensureExamineDetails(DeviceExaminePlan plan) {
|
List<Device> devices = deviceMapper.selectList(null);
|
for (Device device : devices) {
|
fillExamineDetailIfAbsent(plan, device);
|
}
|
}
|
|
/**
|
* 设备本轮应核查日期(没填下次日期时用最近日期+核查周期兜底)
|
*/
|
private LocalDate calcExamineDueDate(Device device) {
|
LocalDateTime base = device.getNextExamineDate();
|
if (base == null && device.getLastExamineDate() != null && StringUtils.isNotBlank(device.getExamineCycle())) {
|
try {
|
base = device.getLastExamineDate().plusMonths(Long.parseLong(device.getExamineCycle().trim()));
|
} catch (NumberFormatException ignored) {
|
}
|
}
|
return base == null ? null : base.toLocalDate();
|
}
|
|
/**
|
* 计划中尚无该设备本轮明细时自动补一条;核查时间取档案下次核查日期(精确到日)
|
*/
|
private void fillExamineDetailIfAbsent(DeviceExaminePlan plan, Device device) {
|
LocalDate due = calcExamineDueDate(device);
|
if (due == null) {
|
return;
|
}
|
if (StringUtils.isBlank(plan.getPlanYear())) {
|
return;
|
}
|
int planYearValue;
|
try {
|
planYearValue = Integer.parseInt(plan.getPlanYear().trim());
|
} catch (NumberFormatException e) {
|
return;
|
}
|
int dueYear = due.getYear();
|
// 未来年份的计划只登记应核查日期就在该年份的设备,避免把往年的旧日期补进未来的计划
|
if (planYearValue > LocalDate.now().getYear()) {
|
if (dueYear != planYearValue) {
|
return;
|
}
|
} else if (dueYear > planYearValue) {
|
return;
|
}
|
String checkTime = due.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
Long exist = deviceExaminePlanDetailsMapper.selectCount(Wrappers.<DeviceExaminePlanDetails>lambdaQuery()
|
.eq(DeviceExaminePlanDetails::getPlanId, plan.getPlanId())
|
.eq(DeviceExaminePlanDetails::getDeviceId, device.getId())
|
.likeRight(DeviceExaminePlanDetails::getCheckTime, checkTime));
|
if (exist != null && exist > 0) {
|
return;
|
}
|
DeviceExaminePlanDetails detail = new DeviceExaminePlanDetails();
|
detail.setPlanId(plan.getPlanId());
|
detail.setDeviceId(device.getId());
|
detail.setDeviceName(device.getDeviceName() == null ? "" : device.getDeviceName());
|
detail.setDeviceModel(device.getSpecificationModel());
|
detail.setDeviceNumber(device.getManagementNumber());
|
detail.setCheckTime(checkTime);
|
deviceExaminePlanDetailsMapper.insert(detail);
|
}
|
|
/**
|
* 为指定设备找一条可录入核查记录的计划明细;没有可用明细时在当前年度计划新增本轮明细
|
*/
|
@Override
|
public DeviceExaminePlanDetails resolveExaminePlanDetail(Integer deviceId, String checkTime) {
|
return findExaminePlanDetail(deviceId, checkTime, true);
|
}
|
|
/**
|
* 查找指定设备可复用的计划明细;只查询,不新增计划或明细
|
*/
|
@Override
|
public DeviceExaminePlanDetails findExaminePlanDetail(Integer deviceId, String checkTime) {
|
return findExaminePlanDetail(deviceId, checkTime, false);
|
}
|
|
private DeviceExaminePlanDetails findExaminePlanDetail(Integer deviceId, String checkTime, boolean createIfAbsent) {
|
if (deviceId == null) {
|
throw new ErrorException("请选择核查设备");
|
}
|
Device device = deviceMapper.selectById(deviceId);
|
if (device == null) {
|
throw new ErrorException("核查设备不存在");
|
}
|
String checkMonth = normalizeCheckMonth(checkTime);
|
String planYear = checkMonth.substring(0, 4);
|
DeviceExaminePlan plan;
|
if (createIfAbsent) {
|
plan = getOrCreatePlanEntity(planYear);
|
} else {
|
plan = baseMapper.selectOne(Wrappers.<DeviceExaminePlan>lambdaQuery()
|
.eq(DeviceExaminePlan::getPlanYear, planYear)
|
.orderByDesc(DeviceExaminePlan::getPlanId)
|
.last("limit 1"));
|
}
|
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 (plan != null && 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;
|
}
|
if (!createIfAbsent) {
|
return null;
|
}
|
DeviceExaminePlanDetails detail = new DeviceExaminePlanDetails();
|
detail.setPlanId(plan.getPlanId());
|
detail.setDeviceId(device.getId());
|
detail.setDeviceName(device.getDeviceName() == null ? "" : device.getDeviceName());
|
detail.setDeviceModel(device.getSpecificationModel());
|
detail.setDeviceNumber(device.getManagementNumber());
|
String detailCheckTime = normalizeCheckDate(checkTime);
|
detail.setCheckTime(StringUtils.isBlank(detailCheckTime) ? checkMonth : detailCheckTime);
|
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"));
|
}
|
|
/**
|
* 规范核查日期:带具体日期的保留到日,只填月份的规范为 yyyy-MM
|
*/
|
private String normalizeCheckDate(String checkTime) {
|
if (StringUtils.isBlank(checkTime)) {
|
return null;
|
}
|
String normalized = checkTime.trim().replace('.', '-').replace('/', '-');
|
if (normalized.length() >= 10) {
|
try {
|
return LocalDate.parse(normalized.substring(0, 10)).toString();
|
} catch (Exception ignored) {
|
}
|
}
|
return normalizeCheckMonth(normalized);
|
}
|
|
/**
|
* 两个月份之间相差的月数
|
*/
|
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 checkTime = normalizeCheckDate(detail.getCheckTime());
|
detail.setCheckTime(checkTime);
|
DeviceExaminePlan plan = getOrCreatePlanEntity(checkTime.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");
|
}
|
DeviceExaminePlanDetails detail = deviceExaminePlanDetailsMapper.selectById(planDetailsId);
|
if (detail == null) {
|
return false;
|
}
|
List<DeviceExamineRecord> records = deviceExamineRecordMapper.selectList(Wrappers.<DeviceExamineRecord>lambdaQuery()
|
.eq(DeviceExamineRecord::getPlanDetailsId, planDetailsId));
|
List<DeviceExamineRecordContrast> contrasts = deviceExamineRecordContrastMapper.selectList(Wrappers.<DeviceExamineRecordContrast>lambdaQuery()
|
.eq(DeviceExamineRecordContrast::getPlanDetailsId, planDetailsId));
|
// 审核不通过的记录允许连同明细一起删除;审核通过的记录为历史留档,不允许删除
|
boolean hasPassedRecord = records.stream().anyMatch(item -> item.getReviewStatus() != null
|
&& item.getReviewStatus() == 1);
|
boolean hasPassedContrast = contrasts.stream().anyMatch(item -> item.getReviewStatus() != null
|
&& item.getReviewStatus() == 1);
|
if (hasPassedRecord || hasPassedContrast) {
|
throw new ErrorException("该计划已存在审核通过的核查记录,为保留历史记录不允许删除");
|
}
|
if (!records.isEmpty()) {
|
List<Integer> recordIds = records.stream().map(DeviceExamineRecord::getRecordId).collect(Collectors.toList());
|
deviceExamineRecordDetailMapper.delete(Wrappers.<DeviceExamineRecordDetail>lambdaQuery()
|
.in(DeviceExamineRecordDetail::getRecordId, recordIds));
|
deviceExamineRecordMapper.deleteBatchIds(recordIds);
|
}
|
if (!contrasts.isEmpty()) {
|
List<Integer> contrastIds = contrasts.stream().map(DeviceExamineRecordContrast::getRecordContrastId).collect(Collectors.toList());
|
deviceExamineRecordContrastDetailsMapper.delete(Wrappers.<DeviceExamineRecordContrastDetails>lambdaQuery()
|
.in(DeviceExamineRecordContrastDetails::getRecordContrastId, contrastIds));
|
deviceExamineRecordContrastMapper.deleteBatchIds(contrastIds);
|
}
|
deviceExamineAttachmentMapper.delete(Wrappers.<DeviceExamineAttachment>lambdaQuery()
|
.eq(DeviceExamineAttachment::getPlanDetailsId, planDetailsId));
|
boolean removed = deviceExaminePlanDetailsService.removeById(planDetailsId);
|
if (removed && detail.getDeviceId() != null) {
|
rollbackDeviceExamineDate(detail);
|
}
|
return removed;
|
}
|
|
/**
|
* 删除未审核的计划后,把设备档案恢复到最近一条审核通过的记录;
|
* 没有已完成记录时,才清空档案的核查日期和周期
|
*/
|
private void rollbackDeviceExamineDate(DeviceExaminePlanDetails removedDetail) {
|
Device device = deviceMapper.selectById(removedDetail.getDeviceId());
|
if (device == null) {
|
return;
|
}
|
Long remainCount = deviceExaminePlanDetailsMapper.selectCount(Wrappers.<DeviceExaminePlanDetails>lambdaQuery()
|
.eq(DeviceExaminePlanDetails::getDeviceId, removedDetail.getDeviceId()));
|
boolean isCurrentRound = sameExamineMonth(removedDetail.getCheckTime(), device.getNextExamineDate());
|
// 删除的不是档案当前登记的那一轮,且设备还有其他计划时,不动档案
|
if (!isCurrentRound && remainCount != null && remainCount > 0) {
|
return;
|
}
|
LocalDate lastDone = null;
|
List<DeviceExaminePlanDetails> details = deviceExaminePlanDetailsMapper.selectList(
|
Wrappers.<DeviceExaminePlanDetails>lambdaQuery()
|
.eq(DeviceExaminePlanDetails::getDeviceId, removedDetail.getDeviceId()));
|
for (DeviceExaminePlanDetails item : details) {
|
if (!hasPassedExamineRecord(item.getPlanDetailsId())) {
|
continue;
|
}
|
LocalDate date = parseCheckMonth(item.getCheckTime());
|
if (date != null && (lastDone == null || date.isAfter(lastDone))) {
|
lastDone = date;
|
}
|
}
|
if (lastDone == null) {
|
deviceMapper.update(null, Wrappers.<Device>lambdaUpdate()
|
.eq(Device::getId, removedDetail.getDeviceId())
|
.set(Device::getLastExamineDate, null)
|
.set(Device::getNextExamineDate, null)
|
.set(Device::getExamineCycle, null));
|
return;
|
}
|
LocalDateTime last = lastDone.atStartOfDay();
|
LocalDateTime next = null;
|
if (StringUtils.isNotBlank(device.getExamineCycle())) {
|
try {
|
next = last.plusMonths(Long.parseLong(device.getExamineCycle().trim()));
|
} catch (NumberFormatException ignored) {
|
}
|
}
|
deviceMapper.update(null, Wrappers.<Device>lambdaUpdate()
|
.eq(Device::getId, removedDetail.getDeviceId())
|
.set(Device::getLastExamineDate, last)
|
.set(Device::getNextExamineDate, next));
|
}
|
|
/**
|
* 明细下是否存在审核通过的核查记录或比对记录
|
*/
|
private boolean hasPassedExamineRecord(Integer planDetailsId) {
|
Long recordCount = deviceExamineRecordMapper.selectCount(Wrappers.<DeviceExamineRecord>lambdaQuery()
|
.eq(DeviceExamineRecord::getPlanDetailsId, planDetailsId)
|
.eq(DeviceExamineRecord::getReviewStatus, 1));
|
if (recordCount != null && recordCount > 0) {
|
return true;
|
}
|
Long contrastCount = deviceExamineRecordContrastMapper.selectCount(Wrappers.<DeviceExamineRecordContrast>lambdaQuery()
|
.eq(DeviceExamineRecordContrast::getPlanDetailsId, planDetailsId)
|
.eq(DeviceExamineRecordContrast::getReviewStatus, 1));
|
return contrastCount != null && contrastCount > 0;
|
}
|
|
/**
|
* 计划明细的核查月份是否就是档案下次核查日期所在月份
|
*/
|
private boolean sameExamineMonth(String checkTime, LocalDateTime nextExamineDate) {
|
if (StringUtils.isBlank(checkTime) || nextExamineDate == null) {
|
return false;
|
}
|
String normalized = checkTime.trim().replace('.', '-').replace('/', '-');
|
return normalized.length() >= 7
|
&& normalized.substring(0, 7).equals(nextExamineDate.format(DateTimeFormatter.ofPattern("yyyy-MM")));
|
}
|
|
/**
|
* 解析明细核查月份为当月首日,格式不合法时返回 null
|
*/
|
private LocalDate parseCheckMonth(String checkTime) {
|
if (StringUtils.isNotBlank(checkTime)) {
|
String normalized = checkTime.trim().replace('.', '-').replace('/', '-');
|
if (normalized.length() >= 7) {
|
try {
|
return YearMonth.parse(normalized.substring(0, 7)).atDay(1);
|
} catch (Exception ignored) {
|
}
|
}
|
}
|
return null;
|
}
|
|
@Override
|
public List<DeviceExamineAttachment> selectAttachments(Integer planDetailsId) {
|
if (planDetailsId == null) {
|
return new ArrayList<>();
|
}
|
return deviceExamineAttachmentMapper.selectList(Wrappers.<DeviceExamineAttachment>lambdaQuery()
|
.eq(DeviceExamineAttachment::getPlanDetailsId, planDetailsId)
|
.orderByAsc(DeviceExamineAttachment::getSort, DeviceExamineAttachment::getId));
|
}
|
|
@Override
|
public DeviceExamineAttachment uploadAttachment(Integer planDetailsId, MultipartFile file) {
|
if (planDetailsId == null) {
|
throw new ErrorException("缺少核查计划明细id");
|
}
|
if (deviceExaminePlanDetailsMapper.selectById(planDetailsId) == null) {
|
throw new ErrorException("核查计划明细不存在");
|
}
|
if (file == null || file.isEmpty()) {
|
throw new ErrorException("附件不能为空");
|
}
|
// 系统全局上传上限为200MB,这里提前给出友好提示
|
if (file.getSize() > 200L * 1024 * 1024) {
|
throw new ErrorException("附件不能超过200MB");
|
}
|
String originalName = file.getOriginalFilename();
|
if (originalName == null || originalName.trim().isEmpty()) {
|
originalName = "未命名文件";
|
}
|
// 个别浏览器可能带上路径前缀,只保留文件名本身
|
originalName = originalName.replaceAll(".*[/\\\\]", "");
|
try {
|
// 图片放图片目录(/img 静态路径),其余任意格式放 word 目录(/word 静态路径)
|
boolean image = originalName.matches("(?i).+\\.(jpg|jpeg|png|gif)$");
|
File directory = new File(image ? filePath : wordUrl);
|
if (!directory.exists()) {
|
directory.mkdirs();
|
}
|
String fileUrl = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmmssSSS")) + "-" + originalName;
|
file.transferTo(new File(directory, fileUrl));
|
DeviceExamineAttachment attachment = new DeviceExamineAttachment();
|
attachment.setPlanDetailsId(planDetailsId);
|
attachment.setFileName(originalName);
|
attachment.setFileUrl(fileUrl);
|
attachment.setSort(selectAttachments(planDetailsId).size());
|
deviceExamineAttachmentMapper.insert(attachment);
|
return attachment;
|
} catch (IOException e) {
|
throw new ErrorException("附件上传失败");
|
}
|
}
|
|
@Override
|
public boolean deleteAttachment(Long id) {
|
return deviceExamineAttachmentMapper.deleteById(id) > 0;
|
}
|
}
|