package com.ruoyi.manage.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.config.Configure; import com.deepoove.poi.config.ConfigureBuilder; import com.deepoove.poi.data.Pictures; import com.ruoyi.common.core.domain.entity.User; import com.ruoyi.common.exception.base.BaseException; import com.ruoyi.common.utils.QueryWrappers; import com.ruoyi.framework.exception.ErrorException; import com.ruoyi.manage.dto.ClientSatisfactionDto; import com.ruoyi.manage.mapper.ClientSatisfactionAnalyseFileMapper; import com.ruoyi.manage.mapper.ClientSatisfactionAttachmentMapper; import com.ruoyi.manage.mapper.ClientSatisfactionMapper; import com.ruoyi.manage.pojo.ClientSatisfaction; import com.ruoyi.manage.pojo.ClientSatisfactionAnalyseFile; import com.ruoyi.manage.pojo.ClientSatisfactionAttachment; import com.ruoyi.manage.service.ClientSatisfactionService; import com.ruoyi.system.mapper.UserMapper; 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.openxmlformats.schemas.wordprocessingml.x2006.main.CTP; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRow; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTc; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URLEncoder; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.List; import java.util.Locale; /** * 客户满意度 * * @author zhuo * @since 2024-11-09 */ @Service public class ClientSatisfactionServiceImpl extends ServiceImpl implements ClientSatisfactionService { @Resource private ClientSatisfactionAnalyseFileMapper clientSatisfactionAnalyseFileMapper; @Resource private ClientSatisfactionAttachmentMapper clientSatisfactionAttachmentMapper; @Resource private UserMapper userMapper; @Value("${file.path}") private String imgUrl; @Value("${wordUrl}") private String wordUrl; /** * 客户满意度调查列表 * * @param page * @param clientSatisfaction * @return */ @Override public IPage pageClientSatisfaction(Page page, ClientSatisfaction clientSatisfaction) { return baseMapper.pageClientSatisfaction(page, QueryWrappers.queryWrappers(clientSatisfaction)); } /** * 新增客户分析附件 * * @param file * @return */ @Override public boolean uploadAnalyseFile(MultipartFile file) { String urlString; String pathName; String path; String filename = file.getOriginalFilename(); String contentType = file.getContentType(); ClientSatisfactionAnalyseFile analyseFile = new ClientSatisfactionAnalyseFile(); analyseFile.setFileName(filename); if (contentType != null && contentType.startsWith("image/")) { // 是图片 path = imgUrl; analyseFile.setType(1); } else { // 是文件 path = wordUrl; analyseFile.setType(2); } try { File realpath = new File(path); if (!realpath.exists()) { realpath.mkdirs(); } pathName = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmmss")) + "_" + file.getOriginalFilename(); urlString = realpath + "/" + pathName; file.transferTo(new File(urlString)); analyseFile.setFileUrl(pathName); clientSatisfactionAnalyseFileMapper.insert(analyseFile); return true; } catch (Exception e) { e.printStackTrace(); throw new ErrorException(e.getMessage()); } } /** * 查询客户分析附件 * * @param page * @param analyseFile * @return */ @Override public IPage pageAnalyseFile(Page page, ClientSatisfactionAnalyseFile analyseFile) { return clientSatisfactionAnalyseFileMapper.pageAnalyseFile(page, QueryWrappers.queryWrappers(analyseFile)); } /** * 客户满意度导出 * * @param clientSatisfactionId * @param response */ @Override public void exportWordClientSatisfaction(Integer clientSatisfactionId, HttpServletResponse response) { // 查询客户满意度调查 ClientSatisfactionDto clientSatisfaction = baseMapper.exportWordClientSatisfaction(clientSatisfactionId); if (clientSatisfaction == null) { throw new RuntimeException("客户满意度调查不存在"); } // 查询确认人的签名 String confirmPersonUrl = null; // 状态为确认且确认人不为空 if (clientSatisfaction.getConfirmStatus() != null && clientSatisfaction.getConfirmStatus() != 0 && clientSatisfaction.getConfirmPersonId() != null) { confirmPersonUrl = userMapper.selectById(clientSatisfaction.getConfirmPersonId()).getSignatureUrl(); if (StringUtils.isBlank(confirmPersonUrl)) { throw new ErrorException("找不到验证人的签名"); } } // 获取路径 InputStream inputStream = this.getClass().getResourceAsStream("/static/customer-satisfaction-questionnaire.docx"); ConfigureBuilder builder = Configure.builder(); String finalConfirmPersonUrl = confirmPersonUrl; XWPFTemplate template = XWPFTemplate.compile(inputStream, builder.build()).render( new HashMap() {{ put("clientSatisfaction", clientSatisfaction); put("finalConfirmPersonUrl", StringUtils.isNotBlank(finalConfirmPersonUrl) ? Pictures.ofLocal(imgUrl + "/" + finalConfirmPersonUrl).create() : null); }}); try { response.setContentType("application/msword"); String fileName = URLEncoder.encode( "客户满意度", "UTF-8"); response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".docx"); OutputStream os = response.getOutputStream(); template.write(os); os.flush(); os.close(); inputStream.close(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("导出失败"); } } /** * 客户满意度年度汇总导出 * * @param year 汇总年度 * @param response */ @Override public void exportAnnualSummary(Integer year, HttpServletResponse response) { int summaryYear = year == null ? LocalDate.now().getYear() : year; List list = baseMapper.selectList(new QueryWrapper().lambda() .ge(ClientSatisfaction::getFillDate, LocalDate.of(summaryYear, 1, 1)) .le(ClientSatisfaction::getFillDate, LocalDate.of(summaryYear, 12, 31)) .isNotNull(ClientSatisfaction::getTotalScore) .orderByAsc(ClientSatisfaction::getFillDate) .orderByAsc(ClientSatisfaction::getClientSatisfactionId)); if (list.isEmpty()) { throw new ErrorException(summaryYear + "年度暂无满意度记录"); } InputStream inputStream = this.getClass().getResourceAsStream("/static/customer-satisfaction-annual-summary.docx"); if (inputStream == null) { throw new ErrorException("年度汇总模板不存在"); } try (InputStream is = inputStream) { XWPFDocument loaded = new XWPFDocument(is); XWPFDocument document = fillAnnualSummaryTable(loaded, summaryYear, list); try (XWPFDocument doc = document) { response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); String fileName = URLEncoder.encode("客户满意度年度汇总表_" + summaryYear + "年.docx", "UTF-8"); response.setHeader("Content-disposition", "attachment;filename=" + fileName); try (OutputStream os = response.getOutputStream()) { doc.write(os); os.flush(); } } } catch (IOException e) { e.printStackTrace(); throw new RuntimeException("导出失败"); } } /** * 按年度汇总模板填充明细与统计数据 */ private XWPFDocument fillAnnualSummaryTable(XWPFDocument document, int year, List list) throws IOException { XWPFTable table = document.getTables().get(0); // 模板固定每页 17 行明细:不足不删空行,超过则另起一页,重复标题与表头后继续填 int dataStart = 3; int templateRows = 17; int total = list.size(); int pages = Math.max(1, (total + templateRows - 1) / templateRows); if (pages > 1) { // 在 XML 层插入续页行;插入后需重新加载文档,POI 的行/单元格包装类才会刷新 CTTbl ctTbl = table.getCTTbl(); CTRow titleRow = table.getRow(0).getCtRow(); CTRow headerRow1 = table.getRow(1).getCtRow(); CTRow headerRow2 = table.getRow(2).getCtRow(); CTRow dataRowSample = table.getRow(dataStart + templateRows - 1).getCtRow(); int insertPos = dataStart + templateRows; for (int page = 1; page < pages; page++) { CTRow newTitleRow = ctTbl.insertNewTr(insertPos++); newTitleRow.set(titleRow.copy()); removeBookmarks(newTitleRow); pageBreakBefore(newTitleRow); CTRow newHeader1 = ctTbl.insertNewTr(insertPos++); newHeader1.set(headerRow1.copy()); CTRow newHeader2 = ctTbl.insertNewTr(insertPos++); newHeader2.set(headerRow2.copy()); for (int r = 0; r < templateRows; r++) { CTRow newRow = ctTbl.insertNewTr(insertPos++); newRow.set(dataRowSample.copy()); } } ByteArrayOutputStream buffer = new ByteArrayOutputStream(); document.write(buffer); document.close(); document = new XWPFDocument(new ByteArrayInputStream(buffer.toByteArray())); table = document.getTables().get(0); } setCellText(table.getRow(0).getCell(0), "汇总年度:" + year + "年"); for (int page = 1; page < pages; page++) { setCellText(table.getRow(page * (templateRows + 3)).getCell(0), "汇总年度:" + year + "年"); } int satisfied = 0; for (int i = 0; i < total; i++) { ClientSatisfaction item = list.get(i); boolean isSatisfied = item.getSatisfactionResult() != null ? item.getSatisfactionResult() == 0 : item.getTotalScore() != null && item.getTotalScore() >= 80; if (isSatisfied) { satisfied++; } int pageIndex = i / templateRows; int indexInPage = i % templateRows; XWPFTableRow row = table.getRow(dataStart + pageIndex * (templateRows + 3) + indexInPage); setCellText(row.getCell(0), String.valueOf(i + 1)); setCellText(row.getCell(1), item.getUnitName()); setCellText(row.getCell(2), isSatisfied ? "√" : ""); setCellText(row.getCell(3), isSatisfied ? "" : "√"); String reason = StringUtils.isNotBlank(item.getLabOpinion()) ? item.getLabOpinion() : item.getRemark(); setCellText(row.getCell(4), isSatisfied ? "" : reason); } XWPFTableRow statRow = table.getRow(dataStart + (pages - 1) * (templateRows + 3) + templateRows + 2); setCellText(statRow.getCell(0), "共 " + total + " 份"); setCellText(statRow.getCell(1), "共 " + satisfied + " 份"); setCellText(statRow.getCell(2), "共 " + (total - satisfied) + " 份"); String percent = String.format(Locale.ROOT, "%.1f", satisfied * 100.0 / total); if (percent.endsWith(".0")) { percent = percent.substring(0, percent.length() - 2); } setCellText(statRow.getCell(3), percent + "%"); return document; } /** * 续页首行设置段前分页,保证每页格子数固定 */ private void pageBreakBefore(CTRow row) { if (row.sizeOfTcArray() == 0) { return; } CTTc cell = row.getTcArray(0); if (cell.sizeOfPArray() == 0) { return; } CTP paragraph = cell.getPArray(0); CTPPr pPr = paragraph.isSetPPr() ? paragraph.getPPr() : paragraph.addNewPPr(); if (!pPr.isSetPageBreakBefore()) { pPr.addNewPageBreakBefore(); } } /** * 删除克隆行内的书签,避免文档出现重复书签 */ private void removeBookmarks(CTRow row) { removeBookmarkNodes(row.getDomNode()); } private void removeBookmarkNodes(Node node) { if (node.getNodeType() == Node.ELEMENT_NODE && ("bookmarkStart".equals(node.getLocalName()) || "bookmarkEnd".equals(node.getLocalName()))) { Node parent = node.getParentNode(); if (parent != null) { parent.removeChild(node); } return; } NodeList children = node.getChildNodes(); for (int i = children.getLength() - 1; i >= 0; i--) { removeBookmarkNodes(children.item(i)); } } /** * 填写单元格文本,保留原有段落格式 */ private void setCellText(XWPFTableCell cell, String text) { while (cell.getParagraphs().size() > 1) { cell.removeParagraph(cell.getParagraphs().size() - 1); } XWPFParagraph paragraph = cell.getParagraphs().get(0); for (int i = paragraph.getRuns().size() - 1; i >= 0; i--) { paragraph.removeRun(i); } XWPFRun run = paragraph.createRun(); run.setText(text == null ? "" : text); run.setFontFamily("宋体"); run.setFontFamily("宋体", XWPFRun.FontCharRange.eastAsia); run.setFontSize(10.5); cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER); } /** * 确认客户满意度 * * @param clientSatisfaction 要修改客户满意度的状态对象 * @param userId 修改人id */ @Override public void confirmClientSatisfaction(ClientSatisfaction clientSatisfaction, Integer userId) { User user = userMapper.selectById(userId); if (user != null) { clientSatisfaction.setConfirmPerson(user.getName()); clientSatisfaction.setConfirmPersonId(user.getId()); } clientSatisfaction.setConfirmStatus(1); baseMapper.updateById(clientSatisfaction); } /** * 查询满意度附件 * * @param clientSatisfactionId 满意度记录id * @return */ @Override public List selectAttachments(Integer clientSatisfactionId) { return clientSatisfactionAttachmentMapper.selectList(new QueryWrapper().lambda() .eq(ClientSatisfactionAttachment::getClientSatisfactionId, clientSatisfactionId) .orderByAsc(ClientSatisfactionAttachment::getSort, ClientSatisfactionAttachment::getId)); } /** * 上传满意度附件 * * @param clientSatisfactionId 满意度记录id * @param file * @return */ @Override public ClientSatisfactionAttachment uploadAttachment(Integer clientSatisfactionId, MultipartFile file) { if (baseMapper.selectCount(new QueryWrapper() .eq("client_satisfaction_id", clientSatisfactionId)) == 0) { throw new BaseException("客户满意度调查不存在"); } if (file == null || file.isEmpty()) { throw new BaseException("附件不能为空"); } // 系统全局上传上限为200MB,这里提前给出友好提示 if (file.getSize() > 200L * 1024 * 1024) { throw new BaseException("附件不能超过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 ? imgUrl : wordUrl); if (!directory.exists()) { directory.mkdirs(); } String fileUrl = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmmssSSS")) + "-" + originalName; file.transferTo(new File(directory, fileUrl)); ClientSatisfactionAttachment attachment = new ClientSatisfactionAttachment(); attachment.setClientSatisfactionId(clientSatisfactionId); attachment.setFileName(originalName); attachment.setFileUrl(fileUrl); attachment.setSort(selectAttachments(clientSatisfactionId).size()); clientSatisfactionAttachmentMapper.insert(attachment); return attachment; } catch (IOException e) { throw new BaseException("附件上传失败"); } } /** * 删除满意度附件 * * @param id 附件id * @return */ @Override public boolean deleteAttachment(Long id) { return clientSatisfactionAttachmentMapper.deleteById(id) > 0; } }