| | |
| | | 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; |
| | | |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 客户满意度年度汇总导出 |
| | | * |
| | | * @param year 汇总年度 |
| | | * @param response |
| | | */ |
| | | @Override |
| | | public void exportAnnualSummary(Integer year, HttpServletResponse response) { |
| | | int summaryYear = year == null ? LocalDate.now().getYear() : year; |
| | | List<ClientSatisfaction> list = baseMapper.selectList(new QueryWrapper<ClientSatisfaction>().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<ClientSatisfaction> 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 要修改客户满意度的状态对象 |