package com.ruoyi.device.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
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.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.SecurityUtils;
import com.ruoyi.common.utils.WxCpUtils;
import com.ruoyi.device.dto.DeviceExamineRecordDto;
import com.ruoyi.device.mapper.DeviceExaminePlanDetailsMapper;
import com.ruoyi.device.mapper.DeviceExamineRecordMapper;
import com.ruoyi.device.mapper.DeviceMapper;
import com.ruoyi.device.pojo.Device;
import com.ruoyi.device.pojo.DeviceExaminePlanDetails;
import com.ruoyi.device.pojo.DeviceExamineRecord;
import com.ruoyi.device.pojo.DeviceExamineRecordDetail;
import com.ruoyi.device.service.DeviceExaminePlanService;
import com.ruoyi.device.service.DeviceExamineRecordDetailService;
import com.ruoyi.device.service.DeviceExamineRecordService;
import com.ruoyi.framework.exception.ErrorException;
import com.ruoyi.system.mapper.UserMapper;
import com.ruoyi.system.service.InformationNotificationService;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.apache.xmlbeans.XmlCursor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.time.LocalDateTime;
import java.util.List;
/**
*
* 设备核查记录表 服务实现类
*
*
* @author 江苏鵷雏网络科技有限公司
* @since 2024-12-16 07:14:28
*/
@Service
@Transactional(rollbackFor = Exception.class)
public class DeviceExamineRecordServiceImpl extends ServiceImpl implements DeviceExamineRecordService {
/** 按设备套用的期间核查记录模板(与设备名称/型号匹配) */
private static final String TEMPLATE_BALANCE_PERCENT = "/static/word/examine/examine-balance-percent.docx";
private static final String TEMPLATE_BALANCE_PERCENT_BSA2202S = "/static/word/examine/examine-balance-percent-bsa2202s.docx";
private static final String TEMPLATE_BALANCE_THOUSANDTH = "/static/word/examine/examine-balance-thousandth.docx";
private static final String TEMPLATE_BALANCE_0_1MG_ME204E = "/static/word/examine/examine-balance-0.1mg-me204e.docx";
private static final String TEMPLATE_BALANCE_0_1MG_AL204 = "/static/word/examine/examine-balance-0.1mg-al204.docx";
private static final String TEMPLATE_SURFACE_TENSION = "/static/word/examine/examine-surface-tension.docx";
private static final String TEMPLATE_ROTARY_VISCOSITY = "/static/word/examine/examine-rotary-viscosity.docx";
private static final String TEMPLATE_VISCOSITY_PIN = "/static/word/examine/examine-viscosity-pin.docx";
private static final String TEMPLATE_SPECTROPHOTOMETER = "/static/word/examine/examine-spectrophotometer.docx";
/** 没有对应设备专用模板时套用的通用期间核查记录表 */
private static final String TEMPLATE_GENERIC = "/static/word/examine/examine-generic.docx";
private static final String WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
private static final String RECORD_NUMBER_TEXT = "记录编号:LMSY-CX09-JL02";
@Resource
private DeviceExamineRecordDetailService deviceExamineRecordDetailService;
@Resource
private DeviceExaminePlanService deviceExaminePlanService;
@Resource
private DeviceExaminePlanDetailsMapper deviceExaminePlanDetailsMapper;
@Resource
private DeviceMapper deviceMapper;
@Resource
private UserMapper userMapper;
@Resource
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Resource
private InformationNotificationService informationNotificationService;
/**
* 查询核查记录
*
* @return
*/
@Override
public DeviceExamineRecordDto getExamineRecord(Integer planDetailsId) {
DeviceExamineRecordDto deviceExamineRecord = baseMapper.getExamineRecord(planDetailsId);
// 判断是否为空
if (deviceExamineRecord == null) {
deviceExamineRecord = new DeviceExamineRecordDto();
// 查询设备详情
DeviceExaminePlanDetails deviceExaminePlanDetails = deviceExaminePlanDetailsMapper.selectById(planDetailsId);
Device device = deviceMapper.selectById(deviceExaminePlanDetails.getDeviceId());
if (device != null) {
deviceExamineRecord.setDeviceName(device.getDeviceName());
deviceExamineRecord.setDeviceNumber(device.getManagementNumber());
deviceExamineRecord.setPlanDetailsId(deviceExaminePlanDetails.getPlanDetailsId());
}
} else {
// 查询详情
List list = deviceExamineRecordDetailService.list(Wrappers.lambdaQuery()
.eq(DeviceExamineRecordDetail::getRecordId, deviceExamineRecord.getRecordId()));
deviceExamineRecord.setRecordDetailList(list);
}
return deviceExamineRecord;
}
/**
* 新增核查记录
*
* @return
*/
@Override
public boolean addExamineRecord(DeviceExamineRecordDto deviceExamineRecordDto) {
if (deviceExamineRecordDto.getPlanDetailsId() == null) {
throw new ErrorException("缺少计划详细信息id");
}
if (deviceExamineRecordDto.getRecordId() == null) {
Integer userId = SecurityUtils.getUserId().intValue();
User user = userMapper.selectById(SecurityUtils.getUserId());
deviceExamineRecordDto.setCheckerUserId(userId);
deviceExamineRecordDto.setCheckerUser(user.getName());
// 查询审查人id
if (deviceExamineRecordDto.getReviewUserId() != null) {
User reviewUser = userMapper.selectById(deviceExamineRecordDto.getReviewUserId());
deviceExamineRecordDto.setReviewUser(reviewUser.getName());
// 消息发送
InformationNotification info = new InformationNotification();
// 发送人
info.setCreateUser(user.getName());
info.setMessageType("6");
info.setTheme("CNAS设备核查计划待批准");
info.setContent("设备编号为: " + deviceExamineRecordDto.getDeviceNumber() + " 设备核查计划待审批");
info.setSenderId(userId);
// 接收人
info.setConsigneeId(deviceExamineRecordDto.getReviewUserId());
info.setJumpPath(MenuJumpPathConstants.DEVICE);
informationNotificationService.addInformationNotification(info);
// 发送企业微信通知
threadPoolTaskExecutor.execute(() -> {
// 查询发送人
User people = userMapper.selectById(deviceExamineRecordDto.getReviewUserId());
String message = "";
message += "CNAS设备核查计划待批准";
message += "\n请去资源要求-设备-设备核查计划审批";
message += "\n" + "设备编号为: " + deviceExamineRecordDto.getDeviceNumber() + "设备核查计划待审批";
//发送企业微信消息通知
try {
WxCpUtils.inform(people.getAccount(), message, null);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}
this.saveOrUpdate(deviceExamineRecordDto);
// 删除全部详情
deviceExamineRecordDetailService.remove(Wrappers.lambdaQuery()
.eq(DeviceExamineRecordDetail::getRecordId, deviceExamineRecordDto.getRecordId()));
//添加详情
if (CollectionUtils.isNotEmpty(deviceExamineRecordDto.getRecordDetailList())) {
for (DeviceExamineRecordDetail deviceExamineRecordDetail : deviceExamineRecordDto.getRecordDetailList()) {
deviceExamineRecordDetail.setRecordId(deviceExamineRecordDto.getRecordId());
}
deviceExamineRecordDetailService.saveBatch(deviceExamineRecordDto.getRecordDetailList());
}
return true;
}
/**
* 复核核查记录
*
* @return
*/
@Override
public boolean reviewExamineRecord(DeviceExamineRecordDto dto) {
if (dto.getPlanDetailsId() == null) {
throw new ErrorException("缺少计划详细信息id");
}
DeviceExaminePlanDetails planDetails = deviceExaminePlanDetailsMapper.selectById(dto.getPlanDetailsId());
if (planDetails == null) {
throw new ErrorException("核查计划明细不存在");
}
if (planDetails.getCheckChargerUserId() == null && StringUtils.isBlank(planDetails.getCheckChargerUser())) {
throw new ErrorException("请先在编辑中填写核查负责人");
}
Integer reviewStatus = dto.getReviewStatus();
Integer reviewerId = SecurityUtils.getUserId().intValue();
User reviewer = userMapper.selectById(reviewerId);
LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate()
.eq(DeviceExamineRecord::getPlanDetailsId, dto.getPlanDetailsId())
.set(DeviceExamineRecord::getReviewStatus, reviewStatus)
.set(DeviceExamineRecord::getReviewRemark, dto.getReviewRemark())
// 审核动作时间即核查完成时间,列表「核查时间」按它显示
.set(DeviceExamineRecord::getUpdateTime, LocalDateTime.now());
// 为0清除审核人
if (reviewStatus != null && reviewStatus == 0) {
wrapper.set(DeviceExamineRecord::getReviewUserId, null)
.set(DeviceExamineRecord::getReviewUser, null);
} else {
wrapper.set(DeviceExamineRecord::getReviewUserId, reviewerId)
.set(DeviceExamineRecord::getReviewUser, reviewer == null ? null : reviewer.getName());
}
boolean updated = this.update(wrapper);
// 计划行尚未录入核查记录时,补一条审核结果记录,保证列表能显示通过/不通过状态
if (!updated) {
DeviceExamineRecord record = new DeviceExamineRecord();
record.setPlanDetailsId(dto.getPlanDetailsId());
record.setReviewStatus(reviewStatus);
record.setReviewRemark(dto.getReviewRemark());
record.setCheckerUserId(planDetails.getCheckChargerUserId());
record.setCheckerUser(planDetails.getCheckChargerUser());
if (reviewStatus != null && reviewStatus == 1) {
record.setReviewUserId(reviewerId);
record.setReviewUser(reviewer == null ? null : reviewer.getName());
}
this.save(record);
}
// 复核通过后,设备档案的最近/下次核查日期按审核当天滚动;计划行的检查日期是计划日期,审核不改它
if (reviewStatus != null && reviewStatus == 1) {
deviceExaminePlanService.updateDeviceExamineDate(dto.getPlanDetailsId());
}
return true;
}
/**
* 导出复核核查记录
*
* @param planDetailsId 复核核查记录id
* @return
*/
@Override
public void exportReviewExamineRecordDetail(Integer planDetailsId, HttpServletResponse response) {
// 查询复核核查记录
DeviceExamineRecordDto deviceExamineRecordDto = baseMapper.selectReviewExamineRecordDto(planDetailsId);
if (deviceExamineRecordDto == null) {
throw new ErrorException("该计划明细暂无核查记录,请先录入后再导出");
}
DeviceExaminePlanDetails planDetails = deviceExaminePlanDetailsMapper.selectById(planDetailsId);
Device device = planDetails != null && planDetails.getDeviceId() != null
? deviceMapper.selectById(planDetails.getDeviceId()) : null;
String deviceName = StringUtils.isNotEmpty(deviceExamineRecordDto.getDeviceName()) ? deviceExamineRecordDto.getDeviceName() : "";
String fileName;
try {
fileName = URLEncoder.encode(deviceName + "期间核查记录.docx", "UTF-8");
} catch (Exception e) {
fileName = "examine-record.docx";
}
// 按设备匹配专用模板,匹配不到时套用通用期间核查记录表
String templatePath = resolveExamineRecordTemplate(planDetails, device);
exportByDeviceTemplate(templatePath, planDetails, deviceExamineRecordDto, fileName, response);
}
/**
* 按设备名称/型号匹配期间核查记录模板
*/
private String resolveExamineRecordTemplate(DeviceExaminePlanDetails planDetails, Device device) {
String deviceName = planDetails == null ? null : planDetails.getDeviceName();
if (StringUtils.isBlank(deviceName) && device != null) {
deviceName = device.getDeviceName();
}
String deviceModel = planDetails == null ? null : planDetails.getDeviceModel();
if (StringUtils.isBlank(deviceModel) && device != null) {
deviceModel = device.getSpecificationModel();
}
String name = deviceName == null ? "" : deviceName.trim();
String model = deviceModel == null ? "" : deviceModel.trim().toUpperCase();
if (name.contains("表面张力")) {
return TEMPLATE_SURFACE_TENSION;
}
if (name.contains("旋转粘度") || name.contains("六速")) {
return TEMPLATE_ROTARY_VISCOSITY;
}
if (name.contains("品氏")) {
return TEMPLATE_VISCOSITY_PIN;
}
if (name.contains("分光光度")) {
return TEMPLATE_SPECTROPHOTOMETER;
}
// 电子天平先按型号细分,再按档位名称
if (model.contains("BSA2202S")) {
return TEMPLATE_BALANCE_PERCENT_BSA2202S;
}
if (model.contains("ME204E")) {
return TEMPLATE_BALANCE_0_1MG_ME204E;
}
if (model.contains("AL204")) {
return TEMPLATE_BALANCE_0_1MG_AL204;
}
if (name.contains("万分之")) {
return TEMPLATE_BALANCE_0_1MG_AL204;
}
if (name.contains("千分之")) {
return TEMPLATE_BALANCE_THOUSANDTH;
}
if (name.contains("百分之")) {
return TEMPLATE_BALANCE_PERCENT;
}
return TEMPLATE_GENERIC;
}
/**
* 按设备专用模板导出:只填表头信息,检测结果表格留空
*/
private void exportByDeviceTemplate(String templatePath, DeviceExaminePlanDetails planDetails,
DeviceExamineRecordDto recordDto, String fileName, HttpServletResponse response) {
try (InputStream inputStream = this.getClass().getResourceAsStream(templatePath)) {
if (inputStream == null) {
throw new ErrorException("核查记录模板不存在:" + templatePath);
}
XWPFDocument document = new XWPFDocument(inputStream);
// 通用表按参考图带记录编号;设备专用模板保持模板原样
if (TEMPLATE_GENERIC.equals(templatePath)) {
insertRecordNumberLine(document);
}
if (!document.getTables().isEmpty()) {
fillTemplateTable(document.getTables().get(0), planDetails, recordDto);
}
response.setContentType("application/msword");
response.setHeader("Content-disposition", "attachment;filename=" + fileName);
OutputStream os = response.getOutputStream();
document.write(os);
os.flush();
os.close();
document.close();
} catch (ErrorException e) {
throw e;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("导出失败");
}
}
/**
* 在标题上方插入固定的记录编号行
*/
private void insertRecordNumberLine(XWPFDocument document) {
List paragraphs = document.getParagraphs();
if (paragraphs.isEmpty()) {
return;
}
XmlCursor cursor = paragraphs.get(0).getCTP().newCursor();
XWPFParagraph paragraph = document.insertNewParagraph(cursor);
paragraph.setAlignment(ParagraphAlignment.LEFT);
XWPFRun run = paragraph.createRun();
run.setFontFamily("宋体");
run.setFontSize(10.5);
run.setText(RECORD_NUMBER_TEXT);
}
/**
* 按标签定位并填写表头,仪器现状打勾,补填评价/审核人
*/
private void fillTemplateTable(XWPFTable table, DeviceExaminePlanDetails planDetails, DeviceExamineRecordDto recordDto) {
String checkDate = StringUtils.isNotEmpty(recordDto.getUpdateTimeStr()) ? recordDto.getUpdateTimeStr()
: (planDetails == null ? "" : planDetails.getCheckTime());
String checkerName = resolveCheckerName(planDetails, recordDto);
String reviewerName = resolveUserName(recordDto.getReviewUserId(), recordDto.getReviewUser());
String appearanceStatus = planDetails == null ? null : planDetails.getAppearanceStatus();
String runningStatus = planDetails == null ? null : planDetails.getRunningStatus();
for (XWPFTableRow row : table.getRows()) {
List cells = row.getTableCells();
for (int i = 0; i < cells.size(); i++) {
String label = normalizeCellLabel(cells.get(i).getText());
if ("外观情况".equals(label)) {
markOptionCells(cells, i + 1, appearanceStatus);
continue;
}
if ("运行是否正常".equals(label)) {
markOptionCells(cells, i + 1, runningStatus);
continue;
}
if (label.isEmpty() || i + 1 >= cells.size()) {
continue;
}
XWPFTableCell valueCell = cells.get(i + 1);
switch (label) {
case "仪器名称":
setCellText(valueCell, planDetails == null ? recordDto.getDeviceName() : planDetails.getDeviceName(), true);
break;
case "管理编号":
case "设备编号":
setCellText(valueCell, planDetails == null ? recordDto.getDeviceNumber() : planDetails.getDeviceNumber(), true);
break;
case "设备型号":
setCellText(valueCell, planDetails == null ? "" : planDetails.getDeviceModel(), true);
break;
case "检查日期":
setCellText(valueCell, checkDate == null ? "" : checkDate, true);
break;
case "核查人":
setCellText(valueCell, checkerName == null ? "" : checkerName, true);
break;
case "环境温湿度":
setCellText(valueCell, planDetails == null ? "" : planDetails.getEnvironment(), true);
break;
case "核查依据":
setCellText(valueCell, planDetails == null ? null : planDetails.getCheckBasis(), false);
break;
case "核查方式":
setCellText(valueCell, planDetails == null ? null : planDetails.getCheckMethod(), false);
break;
default:
break;
}
}
}
fillEvaluationCell(table, reviewerName, checkDate, appearanceStatus, runningStatus);
}
/**
* 仪器现状三选一:选中项显示☑,其余保持□
*/
private void markOptionCells(List cells, int startIndex, String selected) {
for (int i = startIndex; i < cells.size() && i <= startIndex + 2; i++) {
String option = normalizeCellLabel(cells.get(i).getText());
if (!"良好".equals(option) && !"一般".equals(option) && !"损坏".equals(option)
&& !"正常".equals(option) && !"异常".equals(option) && !"停机".equals(option)) {
continue;
}
setCellText(cells.get(i), option + (option.equals(selected) ? "☑" : "□"), true);
}
}
/**
* 核查结果评价勾选 + 审核人/时间填写
*/
private void fillEvaluationCell(XWPFTable table, String reviewerName, String checkDate,
String appearanceStatus, String runningStatus) {
boolean hasStatus = StringUtils.isNotEmpty(appearanceStatus) || StringUtils.isNotEmpty(runningStatus);
boolean satisfied = hasStatus && !"损坏".equals(appearanceStatus)
&& !"异常".equals(runningStatus) && !"停机".equals(runningStatus);
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
for (XWPFParagraph paragraph : cell.getParagraphs()) {
String text = paragraph.getText();
if (text == null) {
continue;
}
if (text.contains("核查结果评价") && text.contains("满足") && hasStatus) {
// 模板里两个复选框依次是“满足”“不满足”
markParagraphSymbol(paragraph, satisfied ? 0 : 1);
}
if (text.contains("审核人")) {
rebuildReviewerParagraph(paragraph, reviewerName, checkDate);
}
}
}
}
}
/**
* 修改段落里第 targetIndex 个 Wingdings 2 复选框为勾选状态(0x52=☑,0xA3=□)
*/
private void markParagraphSymbol(XWPFParagraph paragraph, int targetIndex) {
int index = 0;
for (XWPFRun run : paragraph.getRuns()) {
Node runNode = run.getCTR().getDomNode();
for (Node child = runNode.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child.getNodeType() == Node.ELEMENT_NODE && "sym".equals(child.getLocalName())) {
((Element) child).setAttributeNS(WORD_NS, "w:char", index == targetIndex ? "0052" : "00A3");
index++;
}
}
}
}
/**
* 重写“审核人/时间”行,填入审核人和日期
*/
private void rebuildReviewerParagraph(XWPFParagraph paragraph, String reviewerName, String checkDate) {
String text = paragraph.getText();
int labelIndex = text == null ? -1 : text.indexOf("审核人");
if (labelIndex < 0) {
return;
}
String prefix = text.substring(0, labelIndex);
String dateLabel = text.contains("日期") ? "日期" : "时间";
String newText = prefix + "审核人:" + (reviewerName == null ? "" : reviewerName)
+ " " + dateLabel + ":" + (checkDate == null ? "" : checkDate);
List runs = paragraph.getRuns();
if (runs.isEmpty()) {
XWPFRun run = paragraph.createRun();
run.setFontFamily("宋体");
run.setFontSize(12);
run.setText(newText);
return;
}
runs.get(0).setText(newText, 0);
for (int i = runs.size() - 1; i >= 1; i--) {
paragraph.removeRun(i);
}
}
/**
* 写入单元格文本;overwriteBlank=false 时保留模板里的固定文字
*/
private void setCellText(XWPFTableCell cell, String text, boolean overwriteBlank) {
String value = text == null ? "" : text;
if (!overwriteBlank && value.trim().isEmpty()) {
return;
}
List paragraphs = cell.getParagraphs();
XWPFParagraph paragraph = paragraphs.isEmpty() ? cell.addParagraph() : paragraphs.get(0);
for (int i = paragraphs.size() - 1; i >= 1; i--) {
cell.removeParagraph(i);
}
List runs = paragraph.getRuns();
if (runs.isEmpty()) {
XWPFRun run = paragraph.createRun();
run.setFontFamily("宋体");
run.setFontSize(12);
run.setText(value);
return;
}
runs.get(0).setText(value, 0);
for (int i = runs.size() - 1; i >= 1; i--) {
paragraph.removeRun(i);
}
}
private String normalizeCellLabel(String text) {
if (text == null) {
return "";
}
return text.replace(" ", "").replace("\u00A0", "").replace("□", "").replace("☑", "")
.replace("\n", "").replace("\r", "").trim();
}
private String resolveCheckerName(DeviceExaminePlanDetails planDetails, DeviceExamineRecordDto recordDto) {
if (planDetails != null && StringUtils.isNotBlank(planDetails.getCheckChargerUser())) {
return planDetails.getCheckChargerUser();
}
if (StringUtils.isNotBlank(recordDto.getCheckerUser())) {
return recordDto.getCheckerUser();
}
Integer userId = planDetails != null && planDetails.getCheckChargerUserId() != null
? planDetails.getCheckChargerUserId() : recordDto.getCheckerUserId();
return resolveUserName(userId, null);
}
private String resolveUserName(Integer userId, String userName) {
if (StringUtils.isNotBlank(userName)) {
return userName;
}
if (userId == null) {
return null;
}
User user = userMapper.selectById(userId);
return user == null ? null : user.getName();
}
}